brindle 0.3.5__tar.gz → 0.3.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. {brindle-0.3.5 → brindle-0.3.8}/PKG-INFO +50 -31
  2. {brindle-0.3.5 → brindle-0.3.8}/README.md +49 -30
  3. {brindle-0.3.5 → brindle-0.3.8}/pyproject.toml +6 -2
  4. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/agents.py +39 -9
  5. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/builtin_agents/supervisor.md +2 -0
  6. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/ci_client.py +14 -2
  7. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/cli.py +26 -36
  8. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/company_identity.py +83 -34
  9. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/company_login.py +37 -10
  10. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/cull.py +5 -0
  11. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/events.py +22 -3
  12. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/launch_health.py +16 -3
  13. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/pane_auth.py +3 -3
  14. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/pro/account.py +1 -1
  15. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/pro/audit_chain.py +4 -0
  16. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/pro/audit_export.py +3 -3
  17. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/pro/managed_models.py +184 -26
  18. brindle-0.3.8/src/brindle/pro/route_health.py +249 -0
  19. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/pro/team_events.py +48 -14
  20. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/pro/team_policy.py +126 -27
  21. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/quota.py +118 -10
  22. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/sessions.py +2 -1
  23. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/usage.py +3 -3
  24. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/watch.py +21 -1
  25. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/watch_costs.py +36 -56
  26. {brindle-0.3.5 → brindle-0.3.8}/.gitignore +0 -0
  27. {brindle-0.3.5 → brindle-0.3.8}/LICENSE +0 -0
  28. {brindle-0.3.5 → brindle-0.3.8}/SCHEDULE-A +0 -0
  29. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/__init__.py +0 -0
  30. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/__main__.py +0 -0
  31. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/account.py +0 -0
  32. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/airgap.py +0 -0
  33. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/antigravity.py +0 -0
  34. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/autopilot.py +0 -0
  35. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/budget.py +0 -0
  36. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/builtin_agents/__init__.py +0 -0
  37. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/builtin_agents/backend-auditor.md +0 -0
  38. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/builtin_agents/developer-codex.md +0 -0
  39. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/builtin_agents/developer-fable.md +0 -0
  40. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/builtin_agents/developer-gemini.md +0 -0
  41. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/builtin_agents/developer-heavy.md +0 -0
  42. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/builtin_agents/developer-light.md +0 -0
  43. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/builtin_agents/developer-local.md +0 -0
  44. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/builtin_agents/developer.md +0 -0
  45. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/builtin_agents/qa.md +0 -0
  46. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/builtin_agents/reviewer-codex.md +0 -0
  47. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/builtin_agents/reviewer-gemini.md +0 -0
  48. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/builtin_agents/reviewer-local.md +0 -0
  49. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/builtin_agents/reviewer.md +0 -0
  50. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/builtin_agents/subagent.md +0 -0
  51. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/builtin_rules/__init__.py +0 -0
  52. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/builtin_rules/security/backend.md +0 -0
  53. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/builtin_rules/style/minimal-diff.md +0 -0
  54. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/builtin_rules/tests/only.md +0 -0
  55. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/ci_adapters.py +0 -0
  56. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/ci_federation.py +0 -0
  57. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/ci_hosts.py +0 -0
  58. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/codemap.py +0 -0
  59. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/codex_hook.py +0 -0
  60. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/config.py +0 -0
  61. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/conflicts.py +0 -0
  62. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/cost.py +0 -0
  63. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/cost_estimate.py +0 -0
  64. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/db.py +0 -0
  65. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/demo.py +0 -0
  66. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/detect.py +0 -0
  67. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/doctor.py +0 -0
  68. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/escapes.py +0 -0
  69. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/gates.py +0 -0
  70. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/git.py +0 -0
  71. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/guardrails.py +0 -0
  72. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/history.py +0 -0
  73. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/identity_lock.py +0 -0
  74. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/inbox.py +0 -0
  75. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/keys_cmds.py +0 -0
  76. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/keystore.py +0 -0
  77. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/learned_rules.py +0 -0
  78. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/learning.py +0 -0
  79. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/legacy.py +0 -0
  80. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/mcp_server.py +0 -0
  81. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/model_access.py +0 -0
  82. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/model_availability.py +0 -0
  83. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/native/__init__.py +0 -0
  84. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/native/client.py +0 -0
  85. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/native/loop.py +0 -0
  86. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/native/permissions.py +0 -0
  87. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/native/runner.py +0 -0
  88. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/native/serve.py +0 -0
  89. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/native/tools.py +0 -0
  90. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/permissions.py +0 -0
  91. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/pipeline.py +0 -0
  92. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/plugins.py +0 -0
  93. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/policy.py +0 -0
  94. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/pool.py +0 -0
  95. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/pricing.py +0 -0
  96. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/pro/__init__.py +0 -0
  97. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/pro/_files.py +0 -0
  98. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/pro/auth.py +0 -0
  99. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/pro/brief_learning.py +0 -0
  100. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/pro/cost_centers.py +0 -0
  101. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/pro/credentials.py +0 -0
  102. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/pro/features.py +0 -0
  103. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/pro/keys.py +0 -0
  104. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/pro/learning.py +0 -0
  105. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/pro/license.py +0 -0
  106. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/pro/loopback.py +0 -0
  107. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/pro/org_profiles.py +0 -0
  108. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/pro/orgkey.py +0 -0
  109. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/pro/rollout.py +0 -0
  110. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/pro/settings_sync.py +0 -0
  111. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/pro/status.py +0 -0
  112. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/procs.py +0 -0
  113. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/profiles.py +0 -0
  114. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/providers.py +0 -0
  115. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/repo_cmds.py +0 -0
  116. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/repos.py +0 -0
  117. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/rewind.py +0 -0
  118. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/rule_checks.py +0 -0
  119. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/savings.py +0 -0
  120. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/scratch.py +0 -0
  121. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/secrets.py +0 -0
  122. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/self_serve.py +0 -0
  123. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/services.py +0 -0
  124. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/sidebar_follow.py +0 -0
  125. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/signin_pause.py +0 -0
  126. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/status_cache.py +0 -0
  127. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/tasks.py +0 -0
  128. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/tmux.py +0 -0
  129. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/view.py +0 -0
  130. {brindle-0.3.5 → brindle-0.3.8}/src/brindle/workspaces.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: brindle
3
- Version: 0.3.5
3
+ Version: 0.3.8
4
4
  Summary: Run coding agents in parallel, each on its own git branch, with a supervisor that delegates, reviews, and merges
5
5
  Project-URL: Homepage, https://pawdelta.com/brindle/
6
6
  Project-URL: Documentation, https://pawdelta.com/brindle/docs/recommended-use
@@ -89,7 +89,7 @@ uv tool install --editable ~/Projects/brindle # or from a local checkout
89
89
  brindle drives Claude Code, so you need that too (`npm install -g @anthropic-ai/claude-code`).
90
90
 
91
91
  `brindle --version` prints the installed version; the tmux status bar of every brindle
92
- session shows it too (`brindle 0.3.5`). A session started before an upgrade keeps
92
+ session shows it too (`brindle 0.3.8`). A session started before an upgrade keeps
93
93
  running the old code, and shows the old number, until you restart it.
94
94
 
95
95
  ## Quick start
@@ -118,11 +118,11 @@ a writable home) and, in a repo, what's set up for it, and says what to do about
118
118
  anything missing. Bare `brindle` runs the essential ones (tmux, the chat's CLI)
119
119
  before it launches anything.
120
120
 
121
- That's it. Bare `brindle` opens a fresh interactive developer chat (Claude Code)
122
- in your repo (with autopilot off), with a narrow sidebar on the left showing every
123
- agent: who's working, who's idle, and who's waiting for your approval. To launch an
124
- autopilot supervisor that splits work between parallel workers, reviews, and merges
125
- their branches toward verified milestones, run `brindle supervisor`. It starts in under a second.
121
+ That's it. `brindle` opens a supervisor chat (Claude Code) in your repo, with a
122
+ narrow sidebar on the left showing every agent: who's working, who's idle, and
123
+ who's waiting for your approval. Tell the supervisor what you want. It splits the
124
+ work between workers, each on its own branch, then reviews and merges their
125
+ branches. It starts in under a second.
126
126
 
127
127
  **The sidebar follows you.** There's one sidebar pane per session root, not
128
128
  one per window: switch to any other brindle window or session (⏎ in the
@@ -200,7 +200,7 @@ it from. When the work belongs in a real repository, run `brindle transfer ~/pat
200
200
  merge. Running `brindle` in a repo also offers to bring in any scratch work that hasn't
201
201
  been moved yet.
202
202
 
203
- Or drive a single workspace yourself:
203
+ Or give a supervisor its own workspace:
204
204
 
205
205
  ```sh
206
206
  brindle new fix-login -p "Fix the login redirect bug; add a test"
@@ -242,7 +242,7 @@ for mechanical edits and a Codex reviewer for a second model's opinion.
242
242
 
243
243
  ## Autopilot
244
244
 
245
- `brindle supervisor` starts the supervisor with autopilot on (or `brindle start -a supervisor`). Tell it what we're building,
245
+ `brindle` starts the supervisor with autopilot on. Tell it what we're building,
246
246
  or put the goal in `.brindle/goals.md`, and it works like a project manager:
247
247
 
248
248
  1. **Goal and milestones.** The goal is split into milestones, and each one has
@@ -337,14 +337,14 @@ your own status line prints, so what you see doesn't change.
337
337
 
338
338
  | | |
339
339
  |---|---|
340
- | `brindle` | a fresh developer chat here (autopilot off), dashboard alongside |
341
- | `brindle supervisor` | start an explicit autopilot supervisor chat to coordinate parallel workers and milestones |
340
+ | `brindle` | a fresh supervisor chat here, dashboard alongside |
341
+ | `brindle supervisor` | the same, with `--branch`/`--worktree` to run it in another worktree |
342
342
  | `brindle init` | detect setup and test commands, write `.brindle/config.json`, check tools |
343
343
  | `brindle demo [--local]` | watch brindle finish a tiny practice repo: parallel workers, reviews, gated merges |
344
- | `brindle new BRANCH [-b BASE] [-a PROFILE] [-p PROMPT]` | worktree + branch + agent |
344
+ | `brindle new BRANCH [-b BASE] [-p PROMPT] [--no-agent]` | worktree + branch + a supervisor in it |
345
345
  | `brindle continue [ID]` / `brindle -c` | resume a paused session (default: the most recent) |
346
346
  | `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 |
347
- | `brindle start [-a PROFILE] [-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 |
347
+ | `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 |
348
348
  | `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 |
349
349
  | `brindle autopilot [on\|off\|check]` | the goal's progress; turn autopilot on or off; run the checks now |
350
350
  | `brindle audit-decision FINDING [-r RATIONALE]` | record acceptance of an audit finding with rationale so subsequent audits respect it |
@@ -378,7 +378,7 @@ your own status line prints, so what you see doesn't change.
378
378
  | `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 |
379
379
  | `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 |
380
380
  | `brindle send AGENT MSG` | message an agent; waits in its inbox until it's idle |
381
- | `brindle agent spawn/kill/peek/profiles` | manage agents |
381
+ | `brindle agent kill/peek/profiles` | manage agents (only a supervisor starts them) |
382
382
  | `brindle profile new/lint/show` | write a profile file, check every profile (extends chains, rule packs, patterns), or print one as brindle resolves it |
383
383
  | `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 |
384
384
  | `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 |
@@ -871,7 +871,7 @@ how to use them, and the next step to get the rest:
871
871
  | 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 |
872
872
  | audit log, air-gap | Enterprise | `brindle audit verify`, `"airgap": true` |
873
873
  | audit export | Enterprise | `"audit_export"` in `~/.brindle/config.json`, `brindle audit ship` |
874
- | managed models | Enterprise | `provider_config`, `deny_personal_keys` in your org policy (see "Managed models" below) |
874
+ | managed models | Enterprise | `agent_setup` routes with `enforce`, and `deny_personal_keys`, in your org policy (see "Managed models" below) |
875
875
  | 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) |
876
876
 
877
877
  `brindle ci init` checks your plan before it changes anything: it refuses
@@ -981,20 +981,40 @@ file names, paths or branch names. See `src/brindle/pro/learning.py` and
981
981
 
982
982
  ### Managed models (brindle Enterprise)
983
983
 
984
- An org policy can pick the provider its workers run on and keep people's own
985
- model keys out of their panes. Both fields are ignored without the Enterprise
984
+ An org policy can pick the route its workers run on and keep people's own
985
+ model keys out of their panes. All of this is ignored without the Enterprise
986
986
  entitlement.
987
987
 
988
- * `provider_config`: `provider` (`bedrock`, `vertex`, `azure` or
989
- `openai-compatible`), plus `region`, `model_ids` and `endpoint` as the
990
- provider needs them. For `vertex` set `project` to the GCP project id
991
- (brindle passes it to Claude Code as `ANTHROPIC_VERTEX_PROJECT_ID`; an older
992
- policy that put the project id in `endpoint` still works). Every agent is
993
- launched on that provider, and a profile that points somewhere else is
994
- refused with a message saying why.
995
- * `deny_personal_keys`: the personal model keys (`ANTHROPIC_API_KEY`,
996
- `OPENAI_API_KEY`, ...) are taken out of every agent pane, and a profile that
997
- sets one is refused.
988
+ * The routes live in the policy's `agent_setup` block (the same one `brindle
989
+ login` applies, see "Using your company's access"), one per agent. With
990
+ `enforce` on, an agent whose route points at a cloud or an endpoint of the
991
+ org's is managed: Claude Code on `bedrock` (`aws.region`; the SSO group
992
+ `sso_start_url`, `sso_region`, `account_id` and `role_name` is optional, all
993
+ four or none, and without it your own AWS sign-in applies), `vertex`
994
+ (`gcp.project`, `gcp.region`), `foundry` (`azure.resource` with its
995
+ `subscription_id`, or `azure.base_url`, the endpoint) or `gateway`
996
+ (`gateway.base_url`), with `model_ids` (the ids the route serves) and
997
+ `default_model` (what panes start on, `ANTHROPIC_MODEL`); Codex on `azure`
998
+ or `gateway` with `codex.base_url` (`OPENAI_BASE_URL`). The route's
999
+ variables are the top layer of every pane's environment, a profile that
1000
+ points somewhere else is refused with a message saying why, and the
1001
+ identity is checked at launch. With `exclusive` on as well, an agent the
1002
+ block gives no route at all is refused (Claude Code or Codex with no entry,
1003
+ and always native and Antigravity); without it such an agent runs
1004
+ unmanaged (use `allowed_providers` to keep it out). Without `enforce` the
1005
+ same block is sign-in help only.
1006
+ * `provider_config` is the older, org-wide form of the same thing: `provider`
1007
+ (`bedrock`, `vertex`, `azure` or `openai-compatible`), plus `region`,
1008
+ `model_ids` and `endpoint` as the provider needs them (for `vertex`,
1009
+ `project`; an older policy that put the project id in `endpoint` still
1010
+ works). The server now converts it into `agent_setup` routes (with
1011
+ `exclusive` on) and keeps deriving it for older clients; this client reads
1012
+ it whenever `agent_setup` has no `exclusive` field (a server older than
1013
+ routing in `agent_setup`) or routes nothing. An old
1014
+ `provider_config` and its `agent_setup` form give panes the same variables.
1015
+ * `deny_personal_keys` (in `agent_setup`, or the policy's top-level field):
1016
+ the personal model keys (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, ...) are
1017
+ taken out of every agent pane, and a profile that sets one is refused.
998
1018
  * An org-owned key: with `provider` `anthropic` (or `openai-compatible`), set
999
1019
  `key_env` to the name of a variable that holds the org's key (not a personal
1000
1020
  one like `ANTHROPIC_API_KEY`), or `key_helper` to a command, as a list of
@@ -1013,10 +1033,9 @@ Two things to know when you turn on `deny_personal_keys`:
1013
1033
  `api_key_env` to the name of a variable that holds the org's key (not
1014
1034
  `OPENAI_API_KEY`).
1015
1035
  * It can't block a Claude subscription or keychain login by itself: Claude
1016
- Code signs in on its own, outside the variables brindle removes. Only a
1017
- `provider_config` that routes Claude Code to Bedrock, Vertex or Foundry
1018
- (`azure`) keeps it off that login. Always pair `deny_personal_keys` with a
1019
- `provider_config`.
1036
+ Code signs in on its own, outside the variables brindle removes. Only an
1037
+ enforced route that takes Claude Code to Bedrock, Vertex or Foundry keeps
1038
+ it off that login. Always pair `deny_personal_keys` with one.
1020
1039
 
1021
1040
  ### Air-gapped mode (brindle Enterprise)
1022
1041
 
@@ -59,7 +59,7 @@ uv tool install --editable ~/Projects/brindle # or from a local checkout
59
59
  brindle drives Claude Code, so you need that too (`npm install -g @anthropic-ai/claude-code`).
60
60
 
61
61
  `brindle --version` prints the installed version; the tmux status bar of every brindle
62
- session shows it too (`brindle 0.3.5`). A session started before an upgrade keeps
62
+ session shows it too (`brindle 0.3.8`). A session started before an upgrade keeps
63
63
  running the old code, and shows the old number, until you restart it.
64
64
 
65
65
  ## Quick start
@@ -88,11 +88,11 @@ a writable home) and, in a repo, what's set up for it, and says what to do about
88
88
  anything missing. Bare `brindle` runs the essential ones (tmux, the chat's CLI)
89
89
  before it launches anything.
90
90
 
91
- That's it. Bare `brindle` opens a fresh interactive developer chat (Claude Code)
92
- in your repo (with autopilot off), with a narrow sidebar on the left showing every
93
- agent: who's working, who's idle, and who's waiting for your approval. To launch an
94
- autopilot supervisor that splits work between parallel workers, reviews, and merges
95
- their branches toward verified milestones, run `brindle supervisor`. It starts in under a second.
91
+ That's it. `brindle` opens a supervisor chat (Claude Code) in your repo, with a
92
+ narrow sidebar on the left showing every agent: who's working, who's idle, and
93
+ who's waiting for your approval. Tell the supervisor what you want. It splits the
94
+ work between workers, each on its own branch, then reviews and merges their
95
+ branches. It starts in under a second.
96
96
 
97
97
  **The sidebar follows you.** There's one sidebar pane per session root, not
98
98
  one per window: switch to any other brindle window or session (⏎ in the
@@ -170,7 +170,7 @@ it from. When the work belongs in a real repository, run `brindle transfer ~/pat
170
170
  merge. Running `brindle` in a repo also offers to bring in any scratch work that hasn't
171
171
  been moved yet.
172
172
 
173
- Or drive a single workspace yourself:
173
+ Or give a supervisor its own workspace:
174
174
 
175
175
  ```sh
176
176
  brindle new fix-login -p "Fix the login redirect bug; add a test"
@@ -212,7 +212,7 @@ for mechanical edits and a Codex reviewer for a second model's opinion.
212
212
 
213
213
  ## Autopilot
214
214
 
215
- `brindle supervisor` starts the supervisor with autopilot on (or `brindle start -a supervisor`). Tell it what we're building,
215
+ `brindle` starts the supervisor with autopilot on. Tell it what we're building,
216
216
  or put the goal in `.brindle/goals.md`, and it works like a project manager:
217
217
 
218
218
  1. **Goal and milestones.** The goal is split into milestones, and each one has
@@ -307,14 +307,14 @@ your own status line prints, so what you see doesn't change.
307
307
 
308
308
  | | |
309
309
  |---|---|
310
- | `brindle` | a fresh developer chat here (autopilot off), dashboard alongside |
311
- | `brindle supervisor` | start an explicit autopilot supervisor chat to coordinate parallel workers and milestones |
310
+ | `brindle` | a fresh supervisor chat here, dashboard alongside |
311
+ | `brindle supervisor` | the same, with `--branch`/`--worktree` to run it in another worktree |
312
312
  | `brindle init` | detect setup and test commands, write `.brindle/config.json`, check tools |
313
313
  | `brindle demo [--local]` | watch brindle finish a tiny practice repo: parallel workers, reviews, gated merges |
314
- | `brindle new BRANCH [-b BASE] [-a PROFILE] [-p PROMPT]` | worktree + branch + agent |
314
+ | `brindle new BRANCH [-b BASE] [-p PROMPT] [--no-agent]` | worktree + branch + a supervisor in it |
315
315
  | `brindle continue [ID]` / `brindle -c` | resume a paused session (default: the most recent) |
316
316
  | `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 |
317
- | `brindle start [-a PROFILE] [-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 |
317
+ | `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 |
318
318
  | `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 |
319
319
  | `brindle autopilot [on\|off\|check]` | the goal's progress; turn autopilot on or off; run the checks now |
320
320
  | `brindle audit-decision FINDING [-r RATIONALE]` | record acceptance of an audit finding with rationale so subsequent audits respect it |
@@ -348,7 +348,7 @@ your own status line prints, so what you see doesn't change.
348
348
  | `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 |
349
349
  | `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 |
350
350
  | `brindle send AGENT MSG` | message an agent; waits in its inbox until it's idle |
351
- | `brindle agent spawn/kill/peek/profiles` | manage agents |
351
+ | `brindle agent kill/peek/profiles` | manage agents (only a supervisor starts them) |
352
352
  | `brindle profile new/lint/show` | write a profile file, check every profile (extends chains, rule packs, patterns), or print one as brindle resolves it |
353
353
  | `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 |
354
354
  | `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 |
@@ -841,7 +841,7 @@ how to use them, and the next step to get the rest:
841
841
  | 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 |
842
842
  | audit log, air-gap | Enterprise | `brindle audit verify`, `"airgap": true` |
843
843
  | audit export | Enterprise | `"audit_export"` in `~/.brindle/config.json`, `brindle audit ship` |
844
- | managed models | Enterprise | `provider_config`, `deny_personal_keys` in your org policy (see "Managed models" below) |
844
+ | managed models | Enterprise | `agent_setup` routes with `enforce`, and `deny_personal_keys`, in your org policy (see "Managed models" below) |
845
845
  | 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) |
846
846
 
847
847
  `brindle ci init` checks your plan before it changes anything: it refuses
@@ -951,20 +951,40 @@ file names, paths or branch names. See `src/brindle/pro/learning.py` and
951
951
 
952
952
  ### Managed models (brindle Enterprise)
953
953
 
954
- An org policy can pick the provider its workers run on and keep people's own
955
- model keys out of their panes. Both fields are ignored without the Enterprise
954
+ An org policy can pick the route its workers run on and keep people's own
955
+ model keys out of their panes. All of this is ignored without the Enterprise
956
956
  entitlement.
957
957
 
958
- * `provider_config`: `provider` (`bedrock`, `vertex`, `azure` or
959
- `openai-compatible`), plus `region`, `model_ids` and `endpoint` as the
960
- provider needs them. For `vertex` set `project` to the GCP project id
961
- (brindle passes it to Claude Code as `ANTHROPIC_VERTEX_PROJECT_ID`; an older
962
- policy that put the project id in `endpoint` still works). Every agent is
963
- launched on that provider, and a profile that points somewhere else is
964
- refused with a message saying why.
965
- * `deny_personal_keys`: the personal model keys (`ANTHROPIC_API_KEY`,
966
- `OPENAI_API_KEY`, ...) are taken out of every agent pane, and a profile that
967
- sets one is refused.
958
+ * The routes live in the policy's `agent_setup` block (the same one `brindle
959
+ login` applies, see "Using your company's access"), one per agent. With
960
+ `enforce` on, an agent whose route points at a cloud or an endpoint of the
961
+ org's is managed: Claude Code on `bedrock` (`aws.region`; the SSO group
962
+ `sso_start_url`, `sso_region`, `account_id` and `role_name` is optional, all
963
+ four or none, and without it your own AWS sign-in applies), `vertex`
964
+ (`gcp.project`, `gcp.region`), `foundry` (`azure.resource` with its
965
+ `subscription_id`, or `azure.base_url`, the endpoint) or `gateway`
966
+ (`gateway.base_url`), with `model_ids` (the ids the route serves) and
967
+ `default_model` (what panes start on, `ANTHROPIC_MODEL`); Codex on `azure`
968
+ or `gateway` with `codex.base_url` (`OPENAI_BASE_URL`). The route's
969
+ variables are the top layer of every pane's environment, a profile that
970
+ points somewhere else is refused with a message saying why, and the
971
+ identity is checked at launch. With `exclusive` on as well, an agent the
972
+ block gives no route at all is refused (Claude Code or Codex with no entry,
973
+ and always native and Antigravity); without it such an agent runs
974
+ unmanaged (use `allowed_providers` to keep it out). Without `enforce` the
975
+ same block is sign-in help only.
976
+ * `provider_config` is the older, org-wide form of the same thing: `provider`
977
+ (`bedrock`, `vertex`, `azure` or `openai-compatible`), plus `region`,
978
+ `model_ids` and `endpoint` as the provider needs them (for `vertex`,
979
+ `project`; an older policy that put the project id in `endpoint` still
980
+ works). The server now converts it into `agent_setup` routes (with
981
+ `exclusive` on) and keeps deriving it for older clients; this client reads
982
+ it whenever `agent_setup` has no `exclusive` field (a server older than
983
+ routing in `agent_setup`) or routes nothing. An old
984
+ `provider_config` and its `agent_setup` form give panes the same variables.
985
+ * `deny_personal_keys` (in `agent_setup`, or the policy's top-level field):
986
+ the personal model keys (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, ...) are
987
+ taken out of every agent pane, and a profile that sets one is refused.
968
988
  * An org-owned key: with `provider` `anthropic` (or `openai-compatible`), set
969
989
  `key_env` to the name of a variable that holds the org's key (not a personal
970
990
  one like `ANTHROPIC_API_KEY`), or `key_helper` to a command, as a list of
@@ -983,10 +1003,9 @@ Two things to know when you turn on `deny_personal_keys`:
983
1003
  `api_key_env` to the name of a variable that holds the org's key (not
984
1004
  `OPENAI_API_KEY`).
985
1005
  * It can't block a Claude subscription or keychain login by itself: Claude
986
- Code signs in on its own, outside the variables brindle removes. Only a
987
- `provider_config` that routes Claude Code to Bedrock, Vertex or Foundry
988
- (`azure`) keeps it off that login. Always pair `deny_personal_keys` with a
989
- `provider_config`.
1006
+ Code signs in on its own, outside the variables brindle removes. Only an
1007
+ enforced route that takes Claude Code to Bedrock, Vertex or Foundry keeps
1008
+ it off that login. Always pair `deny_personal_keys` with one.
990
1009
 
991
1010
  ### Air-gapped mode (brindle Enterprise)
992
1011
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "brindle"
3
- version = "0.3.5"
3
+ version = "0.3.8"
4
4
  description = "Run coding agents in parallel, each on its own git branch, with a supervisor that delegates, reviews, and merges"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -51,6 +51,7 @@ audit_export = "brindle.pro.audit_export:make"
51
51
  [dependency-groups]
52
52
  dev = [
53
53
  "pytest>=8",
54
+ "pytest-cov>=7.0",
54
55
  "pytest-xdist>=3.8.0",
55
56
  "python-hcl2>=8.1.4",
56
57
  "pyyaml>=6.0.3",
@@ -74,4 +75,7 @@ only-include = ["src", "README.md", "LICENSE", "SCHEDULE-A", "pyproject.toml"]
74
75
  [tool.pytest.ini_options]
75
76
  testpaths = ["tests"]
76
77
  pythonpath = ["tests"]
77
- markers = ["serial: run outside xdist (`pytest -m serial`); the parallel pass skips these"]
78
+ markers = [
79
+ "serial: run outside xdist (`pytest -m serial`); the parallel pass skips these",
80
+ "variant(reason): an input variant that covers no new code on purpose (see scripts/check_new_tests.py)",
81
+ ]
@@ -24,6 +24,7 @@ from brindle import company_identity, company_login, git, keystore, pane_auth, p
24
24
  from brindle.config import RepoConfig
25
25
  from brindle.db import DB, Agent, Workspace
26
26
  from brindle.model_availability import ModelAvailabilityChecker, PROVISIONING_TIMEOUT, provisioning_context
27
+ from brindle.pro import route_health
27
28
  from brindle.profiles import load_profile, missing_add_dirs
28
29
  from brindle.providers import LaunchContext, get_provider, signed_out, unusable
29
30
  from brindle.sidebar_follow import (
@@ -92,6 +93,9 @@ When you have finished:
92
93
  """
93
94
 
94
95
 
96
+ SUPERVISOR = "supervisor" # the only profile a launch starts
97
+
98
+
95
99
  class AgentError(RuntimeError):
96
100
  pass
97
101
 
@@ -182,11 +186,12 @@ def agent_env(ws: Workspace, agent_id: str, agent: Agent | None = None,
182
186
  if m is _UNREAD:
183
187
  m = managed(ws.repo_root)
184
188
  # Claude Code's own managed settings (apiKeyHelper, a managed env) pick
185
- # the credential and backend: brindle adds no provider_config env there.
189
+ # the credential and backend: brindle adds no managed route env there.
186
190
  org = agent.provider == "claude" and providers.claude_org_managed()
187
- # The company's Claude Code env (`brindle login`): the lowest layer, so a
188
- # profile's env lines and the org's managed provider both beat it; none
189
- # at all under Claude Code's managed settings (stored_env).
191
+ # The company's Claude Code env (`brindle login`, the org's agent_setup
192
+ # without `enforce`): the lowest layer, so a profile's env lines and the
193
+ # org's managed route (an enforced agent_setup, or provider_config) both
194
+ # beat it; none at all under Claude Code's managed settings (stored_env).
190
195
  company = {} if agent.provider != "claude" else company_login.stored_env()
191
196
  managed_env = {} if org else managed_models.agent_env(m, agent.provider)
192
197
  enforced = None if agent.provider != "claude" or org else company_identity.enforced_route_env()
@@ -236,7 +241,8 @@ def agent_env(ws: Workspace, agent_id: str, agent: Agent | None = None,
236
241
  env = {**company, **recovery_keys, **profile_env, **managed_env, **env}
237
242
  else:
238
243
  # An enforced company identity (Enterprise): the pane runs on the route
239
- # launch_problem checked, whatever a profile's env lines say.
244
+ # launch_problem checked, whatever a profile's env lines say. The route
245
+ # env is the top layer (managed_env is the same route's, from managed_models).
240
246
  profile_env = {k: v for k, v in profile_env.items() if company_identity.profile_may_set(k)}
241
247
  env = {**recovery_keys, **profile_env, **managed_env, **enforced, **env}
242
248
  if agent is not None and preload_tools(agent, ws):
@@ -528,9 +534,10 @@ def _window_environment(agent: Agent, ws: Workspace, m=_UNREAD):
528
534
  raise AgentError(f"managed models: {e}") from None
529
535
  # An enforced company identity: also every account picker the tmux server
530
536
  # would hand the pane or `brindle keys` holds for it (pane_auth covers this process's).
531
- route = None if agent.provider != "claude" or org_managed else company_identity.enforced_route_env()
537
+ route = None if agent.provider != "claude" or org_managed else company_identity.enforced_route()
532
538
  if route is not None:
533
- extra = company_identity.enforced_deny(route, tmux.inherited_names(ws.tmux_session), credentials)
539
+ extra = company_identity.enforced_deny(route[0], tmux.inherited_names(ws.tmux_session), credentials,
540
+ own=route[1])
534
541
  deny = (*deny, *(n for n in extra if n not in deny))
535
542
  # Claude Code's managed settings supply the credential: no stored Claude key.
536
543
  no_stored = set(deny) | (set(providers._ENV_AUTH["claude"]) if org_managed else set())
@@ -566,6 +573,9 @@ def model_problem(profile_name: str, ws: Workspace, *, agent: Agent | None = Non
566
573
  profile = guardrails.effective(load_profile(profile_name, ws.repo_root))
567
574
  why = managed_models.refusal(m, profile) if m is not None else None
568
575
  if why:
576
+ route_health.record(ws.repo_root, agent=profile.provider,
577
+ route=route_health.route_of(profile.provider, managed_models.agent_env(m, profile.provider), m),
578
+ model=profile.model, outcome="policy_refused")
569
579
  return f"managed models: {why}"
570
580
  if m is not None:
571
581
  profile = managed_models.apply_profile(m, profile)
@@ -583,7 +593,24 @@ def model_problem(profile_name: str, ws: Workspace, *, agent: Agent | None = Non
583
593
  account_identity=account_revision(profile.provider, effective),
584
594
  catalog_context=provisioning_context(profile.provider, ws.path, effective),
585
595
  timeout=PROVISIONING_TIMEOUT)
586
- return result.explanation if result.status == "unavailable" else None
596
+ if result.status != "unavailable":
597
+ return None
598
+ route_health.record(ws.repo_root, agent=profile.provider,
599
+ route=route_health.route_of(profile.provider, effective, m),
600
+ model=profile.model, outcome="model_unavailable")
601
+ return result.explanation
602
+
603
+
604
+ def route_context(db: DB, agent: Agent, ws: Workspace) -> tuple[str, str | None]:
605
+ """(the route ``agent``'s calls take, its model) for a route-health event
606
+ about a run; (``"unknown"``, None) when they can't be read."""
607
+ try:
608
+ m = managed(ws.repo_root)
609
+ profile = _profile_for(db, agent, ws, m)
610
+ env = _effective_window_env(agent, ws, m)
611
+ return route_health.route_of(agent.provider, env, m), profile.model
612
+ except Exception: # noqa: BLE001 - a report about a run never fails over its context
613
+ return "unknown", None
587
614
 
588
615
 
589
616
  _model_checker = ModelAvailabilityChecker()
@@ -741,7 +768,10 @@ def _launch(db: DB, agent: Agent, ws: Workspace, *, prompt: str | None,
741
768
  return
742
769
  # An org that enforces its company sign-in (Enterprise) refuses a
743
770
  # different identity; one that can't be told never blocks.
744
- why = company_identity.launch_problem(provider.name)
771
+ why, outcome, route = company_identity.launch_check(provider.name)
772
+ if outcome:
773
+ route_health.record(ws.repo_root, agent=provider.name, route=route, model=profile.model,
774
+ outcome=outcome)
745
775
  if why:
746
776
  raise AgentError(why)
747
777
  from brindle import signin_pause
@@ -13,6 +13,8 @@ How to work:
13
13
  report.
14
14
  - Break the request into independent, well-scoped tasks. Tasks that touch the
15
15
  same files should go to one worker, or run one after another.
16
+ - Delegate to developers and other worker profiles. Start another supervisor
17
+ only when a piece of work is big enough to need its own workers, which is rare.
16
18
  - Delegate with the brindle MCP tools. `assign` runs workers in parallel (their
17
19
  results arrive later as messages). `handoff` waits for a single result.
18
20
  Leave `isolate` on: each worker gets its own git worktree and branch cut
@@ -1995,14 +1995,26 @@ def _set_cloud(repo: str, cloud: str, values: Mapping[str, str], *, run=subproce
1995
1995
 
1996
1996
 
1997
1997
  def managed_cloud(cwd: str) -> tuple[str, dict[str, str]] | None:
1998
- """On Enterprise, the cloud the org policy's ``provider_config`` names
1999
- (bedrock, vertex or azure) and the region/project to prefill; else None."""
1998
+ """On Enterprise, the cloud the org policy routes Claude to (its enforced
1999
+ ``agent_setup`` route, else its ``provider_config``: bedrock, vertex or
2000
+ azure) and the region/project to prefill; else None."""
2000
2001
  from brindle.pro import managed_models
2001
2002
 
2002
2003
  try:
2003
2004
  m = managed_models.current(cwd)
2004
2005
  except Exception: # noqa: BLE001 - not managed, or unreadable: no default
2005
2006
  return None
2007
+ claude = m.claude if m else None
2008
+ if claude is not None:
2009
+ cloud = {"bedrock": BEDROCK, "vertex": VERTEX, "foundry": FOUNDRY}.get(claude.route)
2010
+ if not cloud:
2011
+ return None
2012
+ if cloud == BEDROCK:
2013
+ return cloud, {"AWS_REGION": claude.aws.region}
2014
+ if cloud == VERTEX:
2015
+ return cloud, {"CLOUD_ML_REGION": claude.gcp.region,
2016
+ "ANTHROPIC_VERTEX_PROJECT_ID": claude.gcp.project}
2017
+ return cloud, {}
2006
2018
  cfg = m.config if m else None
2007
2019
  cloud = POLICY_CLOUDS.get(cfg.provider) if cfg else None
2008
2020
  if not cloud: