brindle 0.3.18__tar.gz → 0.3.19__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 (141) hide show
  1. brindle-0.3.18/README.md → brindle-0.3.19/PKG-INFO +58 -1
  2. brindle-0.3.18/PKG-INFO → brindle-0.3.19/README.md +27 -32
  3. {brindle-0.3.18 → brindle-0.3.19}/pyproject.toml +1 -1
  4. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/cli.py +104 -4
  5. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/config.py +13 -0
  6. brindle-0.3.19/src/brindle/key_check.py +66 -0
  7. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/keys_cmds.py +18 -1
  8. brindle-0.3.19/src/brindle/local_setup.py +259 -0
  9. brindle-0.3.19/src/brindle/picker.py +284 -0
  10. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/pro/team_policy.py +86 -0
  11. brindle-0.3.19/src/brindle/worker_models.py +176 -0
  12. {brindle-0.3.18 → brindle-0.3.19}/.gitignore +0 -0
  13. {brindle-0.3.18 → brindle-0.3.19}/LICENSE +0 -0
  14. {brindle-0.3.18 → brindle-0.3.19}/SCHEDULE-A +0 -0
  15. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/__init__.py +0 -0
  16. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/__main__.py +0 -0
  17. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/account.py +0 -0
  18. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/agents.py +0 -0
  19. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/airgap.py +0 -0
  20. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/antigravity.py +0 -0
  21. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/autopilot.py +0 -0
  22. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/budget.py +0 -0
  23. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/builtin_agents/__init__.py +0 -0
  24. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/builtin_agents/backend-auditor.md +0 -0
  25. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/builtin_agents/developer-codex.md +0 -0
  26. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/builtin_agents/developer-fable.md +0 -0
  27. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/builtin_agents/developer-gemini.md +0 -0
  28. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/builtin_agents/developer-heavy.md +0 -0
  29. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/builtin_agents/developer-light.md +0 -0
  30. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/builtin_agents/developer-local.md +0 -0
  31. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/builtin_agents/developer.md +0 -0
  32. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/builtin_agents/qa.md +0 -0
  33. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/builtin_agents/reviewer-codex.md +0 -0
  34. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/builtin_agents/reviewer-gemini.md +0 -0
  35. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/builtin_agents/reviewer-local.md +0 -0
  36. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/builtin_agents/reviewer.md +0 -0
  37. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/builtin_agents/subagent.md +0 -0
  38. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/builtin_agents/supervisor.md +0 -0
  39. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/builtin_rules/__init__.py +0 -0
  40. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/builtin_rules/security/backend.md +0 -0
  41. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/builtin_rules/style/minimal-diff.md +0 -0
  42. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/builtin_rules/tests/only.md +0 -0
  43. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/ci_adapters.py +0 -0
  44. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/ci_client.py +0 -0
  45. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/ci_federation.py +0 -0
  46. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/ci_hosts.py +0 -0
  47. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/cloud_credential.py +0 -0
  48. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/cloud_login.py +0 -0
  49. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/codemap.py +0 -0
  50. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/codemap_docs.py +0 -0
  51. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/codex_hook.py +0 -0
  52. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/company_identity.py +0 -0
  53. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/company_login.py +0 -0
  54. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/conflicts.py +0 -0
  55. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/cost.py +0 -0
  56. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/cost_estimate.py +0 -0
  57. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/cull.py +0 -0
  58. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/db.py +0 -0
  59. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/demo.py +0 -0
  60. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/detect.py +0 -0
  61. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/doctor.py +0 -0
  62. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/escapes.py +0 -0
  63. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/events.py +0 -0
  64. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/gates.py +0 -0
  65. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/git.py +0 -0
  66. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/guardrails.py +0 -0
  67. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/history.py +0 -0
  68. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/identity_lock.py +0 -0
  69. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/inbox.py +0 -0
  70. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/keystore.py +0 -0
  71. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/launch_health.py +0 -0
  72. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/learned_rules.py +0 -0
  73. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/learning.py +0 -0
  74. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/legacy.py +0 -0
  75. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/login_pause.py +0 -0
  76. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/mcp_server.py +0 -0
  77. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/model_access.py +0 -0
  78. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/model_availability.py +0 -0
  79. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/native/__init__.py +0 -0
  80. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/native/client.py +0 -0
  81. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/native/loop.py +0 -0
  82. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/native/permissions.py +0 -0
  83. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/native/runner.py +0 -0
  84. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/native/serve.py +0 -0
  85. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/native/tools.py +0 -0
  86. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/pane_auth.py +0 -0
  87. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/permissions.py +0 -0
  88. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/pipeline.py +0 -0
  89. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/plugins.py +0 -0
  90. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/policy.py +0 -0
  91. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/pool.py +0 -0
  92. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/pricing.py +0 -0
  93. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/pro/__init__.py +0 -0
  94. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/pro/_files.py +0 -0
  95. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/pro/account.py +0 -0
  96. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/pro/audit_chain.py +0 -0
  97. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/pro/audit_export.py +0 -0
  98. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/pro/auth.py +0 -0
  99. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/pro/brief_learning.py +0 -0
  100. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/pro/cost_centers.py +0 -0
  101. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/pro/credentials.py +0 -0
  102. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/pro/features.py +0 -0
  103. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/pro/keys.py +0 -0
  104. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/pro/learning.py +0 -0
  105. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/pro/license.py +0 -0
  106. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/pro/loopback.py +0 -0
  107. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/pro/managed_models.py +0 -0
  108. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/pro/org_profiles.py +0 -0
  109. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/pro/orgkey.py +0 -0
  110. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/pro/rollout.py +0 -0
  111. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/pro/route_health.py +0 -0
  112. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/pro/settings_sync.py +0 -0
  113. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/pro/status.py +0 -0
  114. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/pro/team_events.py +0 -0
  115. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/procs.py +0 -0
  116. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/profiles.py +0 -0
  117. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/prompt_answer.py +0 -0
  118. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/providers.py +0 -0
  119. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/quota.py +0 -0
  120. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/repo_cmds.py +0 -0
  121. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/repos.py +0 -0
  122. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/rewind.py +0 -0
  123. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/rule_checks.py +0 -0
  124. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/savings.py +0 -0
  125. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/scratch.py +0 -0
  126. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/secrets.py +0 -0
  127. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/self_serve.py +0 -0
  128. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/services.py +0 -0
  129. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/sessions.py +0 -0
  130. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/sidebar_follow.py +0 -0
  131. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/signin_pause.py +0 -0
  132. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/status_cache.py +0 -0
  133. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/tasks.py +0 -0
  134. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/tmux.py +0 -0
  135. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/updates.py +0 -0
  136. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/upgrade.py +0 -0
  137. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/usage.py +0 -0
  138. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/view.py +0 -0
  139. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/watch.py +0 -0
  140. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/watch_costs.py +0 -0
  141. {brindle-0.3.18 → brindle-0.3.19}/src/brindle/workspaces.py +0 -0
@@ -1,3 +1,34 @@
1
+ Metadata-Version: 2.4
2
+ Name: brindle
3
+ Version: 0.3.19
4
+ Summary: Run coding agents in parallel, each on its own git branch, with a supervisor that delegates, reviews, and merges
5
+ Project-URL: Homepage, https://pawdelta.com/brindle/
6
+ Project-URL: Documentation, https://pawdelta.com/brindle/docs/recommended-use
7
+ Project-URL: Changelog, https://pawdelta.com/brindle/changelog
8
+ Project-URL: Support, https://pawdelta.com/brindle/docs/faq
9
+ Author: Hannah Lerner
10
+ License-Expression: LicenseRef-brindle-1.0
11
+ License-File: LICENSE
12
+ License-File: SCHEDULE-A
13
+ Keywords: agents,ai,claude-code,codex,git,mcp,orchestration,tmux,worktree
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Environment :: Console
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: MacOS
18
+ Classifier: Operating System :: POSIX :: Linux
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Software Development
24
+ Classifier: Topic :: Software Development :: Version Control :: Git
25
+ Requires-Python: >=3.11
26
+ Requires-Dist: cryptography>=42
27
+ Requires-Dist: mcp<3,>=2.2
28
+ Requires-Dist: packaging>=24
29
+ Requires-Dist: typer>=0.12
30
+ Description-Content-Type: text/markdown
31
+
1
32
  # brindle
2
33
 
3
34
  *[pawdelta.com/brindle](https://pawdelta.com/brindle/) · Published on PyPI as `brindle`; the command is `brindle`. This project is
@@ -63,7 +94,7 @@ uv tool install --editable ~/Projects/brindle # or from a local checkout
63
94
  brindle drives Claude Code, so you need that too (`npm install -g @anthropic-ai/claude-code`).
64
95
 
65
96
  `brindle --version` prints the installed version; the tmux status bar of every brindle
66
- session shows it too (`brindle 0.3.18`). A session started before an upgrade keeps
97
+ session shows it too (`brindle 0.3.19`). A session started before an upgrade keeps
67
98
  running the old code, and shows the old number, until you restart it.
68
99
 
69
100
  ## Quick start
@@ -184,6 +215,31 @@ brindle pr fix-login # push + gh pr create
184
215
  brindle rm fix-login # deletes the branch only if it's merged (-K keeps it)
185
216
  ```
186
217
 
218
+ ### Choosing the supervisor and workers
219
+
220
+ The first time you run `brindle` it asks which CLI runs the supervisor (the
221
+ agent that plans the work and hands it out), and remembers your pick. After
222
+ that it starts straight away and prints one line naming the CLI. It asks again
223
+ only when your pick is no longer signed in or no longer allowed.
224
+
225
+ - `brindle -s` opens the picker; `brindle -s codex` switches directly. Either
226
+ is remembered.
227
+ - In the picker, `w` opens the workers screen: a model for each task size
228
+ (light, medium, heavy). Models are grouped by how they run: through a CLI
229
+ you sign in to, locally with Ollama, or hosted with an API key. Picking one
230
+ that isn't ready sets it up right there: signing in, installing Ollama and
231
+ downloading the model after a memory check, or asking for the API key.
232
+ - API keys are checked with the provider before they're stored, and stay in
233
+ this machine's keychain.
234
+ - The picker and the workers screen list only the CLIs and models you're
235
+ allowed to use. In an org that means its allowed providers and models for
236
+ your role, its managed routes, and whether personal API keys are allowed.
237
+ `-s` with anything else is refused, and saved picks are re-checked at every
238
+ launch.
239
+ - Without a terminal (scripts, CI), with `--yes`, or in `brindle demo`, nothing
240
+ asks: brindle uses your last pick, else the supervisor profile's CLI (Claude
241
+ Code unless you changed it).
242
+
187
243
  ## Recommended use
188
244
 
189
245
  brindle pays off when work splits into pieces that can proceed in parallel, or
@@ -312,6 +368,7 @@ your own status line prints, so what you see doesn't change.
312
368
  | | |
313
369
  |---|---|
314
370
  | `brindle` | a fresh supervisor chat here, dashboard alongside |
371
+ | `brindle -s [CLI]` | pick the supervisor's CLI (no value: open the picker; `w` there picks worker models); remembered for next time |
315
372
  | `brindle supervisor` | the same, with `--branch`/`--worktree` to run it in another worktree |
316
373
  | `brindle init` | detect setup and test commands, write `.brindle/config.json`, check tools |
317
374
  | `brindle demo [--local]` | watch brindle finish a tiny practice repo: parallel workers, reviews, gated merges |
@@ -1,34 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: brindle
3
- Version: 0.3.18
4
- Summary: Run coding agents in parallel, each on its own git branch, with a supervisor that delegates, reviews, and merges
5
- Project-URL: Homepage, https://pawdelta.com/brindle/
6
- Project-URL: Documentation, https://pawdelta.com/brindle/docs/recommended-use
7
- Project-URL: Changelog, https://pawdelta.com/brindle/changelog
8
- Project-URL: Support, https://pawdelta.com/brindle/docs/faq
9
- Author: Hannah Lerner
10
- License-Expression: LicenseRef-brindle-1.0
11
- License-File: LICENSE
12
- License-File: SCHEDULE-A
13
- Keywords: agents,ai,claude-code,codex,git,mcp,orchestration,tmux,worktree
14
- Classifier: Development Status :: 3 - Alpha
15
- Classifier: Environment :: Console
16
- Classifier: Intended Audience :: Developers
17
- Classifier: Operating System :: MacOS
18
- Classifier: Operating System :: POSIX :: Linux
19
- Classifier: Programming Language :: Python :: 3
20
- Classifier: Programming Language :: Python :: 3.11
21
- Classifier: Programming Language :: Python :: 3.12
22
- Classifier: Programming Language :: Python :: 3.13
23
- Classifier: Topic :: Software Development
24
- Classifier: Topic :: Software Development :: Version Control :: Git
25
- Requires-Python: >=3.11
26
- Requires-Dist: cryptography>=42
27
- Requires-Dist: mcp<3,>=2.2
28
- Requires-Dist: packaging>=24
29
- Requires-Dist: typer>=0.12
30
- Description-Content-Type: text/markdown
31
-
32
1
  # brindle
33
2
 
34
3
  *[pawdelta.com/brindle](https://pawdelta.com/brindle/) · Published on PyPI as `brindle`; the command is `brindle`. This project is
@@ -94,7 +63,7 @@ uv tool install --editable ~/Projects/brindle # or from a local checkout
94
63
  brindle drives Claude Code, so you need that too (`npm install -g @anthropic-ai/claude-code`).
95
64
 
96
65
  `brindle --version` prints the installed version; the tmux status bar of every brindle
97
- session shows it too (`brindle 0.3.18`). A session started before an upgrade keeps
66
+ session shows it too (`brindle 0.3.19`). A session started before an upgrade keeps
98
67
  running the old code, and shows the old number, until you restart it.
99
68
 
100
69
  ## Quick start
@@ -215,6 +184,31 @@ brindle pr fix-login # push + gh pr create
215
184
  brindle rm fix-login # deletes the branch only if it's merged (-K keeps it)
216
185
  ```
217
186
 
187
+ ### Choosing the supervisor and workers
188
+
189
+ The first time you run `brindle` it asks which CLI runs the supervisor (the
190
+ agent that plans the work and hands it out), and remembers your pick. After
191
+ that it starts straight away and prints one line naming the CLI. It asks again
192
+ only when your pick is no longer signed in or no longer allowed.
193
+
194
+ - `brindle -s` opens the picker; `brindle -s codex` switches directly. Either
195
+ is remembered.
196
+ - In the picker, `w` opens the workers screen: a model for each task size
197
+ (light, medium, heavy). Models are grouped by how they run: through a CLI
198
+ you sign in to, locally with Ollama, or hosted with an API key. Picking one
199
+ that isn't ready sets it up right there: signing in, installing Ollama and
200
+ downloading the model after a memory check, or asking for the API key.
201
+ - API keys are checked with the provider before they're stored, and stay in
202
+ this machine's keychain.
203
+ - The picker and the workers screen list only the CLIs and models you're
204
+ allowed to use. In an org that means its allowed providers and models for
205
+ your role, its managed routes, and whether personal API keys are allowed.
206
+ `-s` with anything else is refused, and saved picks are re-checked at every
207
+ launch.
208
+ - Without a terminal (scripts, CI), with `--yes`, or in `brindle demo`, nothing
209
+ asks: brindle uses your last pick, else the supervisor profile's CLI (Claude
210
+ Code unless you changed it).
211
+
218
212
  ## Recommended use
219
213
 
220
214
  brindle pays off when work splits into pieces that can proceed in parallel, or
@@ -343,6 +337,7 @@ your own status line prints, so what you see doesn't change.
343
337
  | | |
344
338
  |---|---|
345
339
  | `brindle` | a fresh supervisor chat here, dashboard alongside |
340
+ | `brindle -s [CLI]` | pick the supervisor's CLI (no value: open the picker; `w` there picks worker models); remembered for next time |
346
341
  | `brindle supervisor` | the same, with `--branch`/`--worktree` to run it in another worktree |
347
342
  | `brindle init` | detect setup and test commands, write `.brindle/config.json`, check tools |
348
343
  | `brindle demo [--local]` | watch brindle finish a tiny practice repo: parallel workers, reviews, gated merges |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "brindle"
3
- version = "0.3.18"
3
+ version = "0.3.19"
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"
@@ -34,7 +34,34 @@ class _App(typer.Typer):
34
34
  raise SystemExit(1)
35
35
 
36
36
 
37
- app = _App(add_completion=False, help="""brindle: run coding agents in parallel, each on its own git branch.
37
+ SUPERVISOR_FLAGS = ("-s", "--supervisor")
38
+
39
+
40
+ def _bare_supervisor_flag(args: list[str], commands) -> list[str]:
41
+ """``args`` with picker.PICK after each ``-s``/``--supervisor`` that has
42
+ no CLI after it (it's last, or an option or a command follows), so a bare
43
+ ``brindle -s`` opens the picker: a Typer option can't take an optional
44
+ value."""
45
+ from brindle.picker import PICK
46
+
47
+ out = []
48
+ for i, a in enumerate(args):
49
+ out.append(a)
50
+ if a == "--":
51
+ return out + list(args[i + 1:])
52
+ if a in SUPERVISOR_FLAGS:
53
+ nxt = args[i + 1] if i + 1 < len(args) else None
54
+ if nxt is None or nxt.startswith("-") or nxt in commands:
55
+ out.append(PICK)
56
+ return out
57
+
58
+
59
+ class _PickerGroup(typer.core.TyperGroup):
60
+ def parse_args(self, ctx, args):
61
+ return super().parse_args(ctx, _bare_supervisor_flag(args, self.commands))
62
+
63
+
64
+ app = _App(cls=_PickerGroup, add_completion=False, help="""brindle: run coding agents in parallel, each on its own git branch.
38
65
 
39
66
  Run `brindle` with no arguments to open (or reopen) a supervisor chat here, with
40
67
  the live dashboard underneath. Outside a git repo it starts a scratch session;
@@ -177,7 +204,7 @@ def demo(
177
204
  typer.echo(f"demo repo: {root}")
178
205
  os.chdir(root)
179
206
  start(prompt=None, provider=None, attach=attach, watch=True,
180
- autopilot=True, branch=None, worktree=None)
207
+ autopilot=True, branch=None, worktree=None, demo=True)
181
208
 
182
209
 
183
210
  @app.command()
@@ -193,6 +220,7 @@ def new(
193
220
  no_fetch: bool = typer.Option(False, "--no-fetch", help="Don't fetch the base branch first."),
194
221
  no_setup: bool = typer.Option(False, "--no-setup", help="Skip setup commands."),
195
222
  attach: bool = typer.Option(False, "--attach", help="Attach to the tmux session afterward."),
223
+ yes: Annotated[bool, typer.Option("--yes", "-y", help="Don't open the supervisor picker: use your last pick, else the default.")] = False,
196
224
  provider_alias: Annotated[Optional[str], typer.Option("--provider", hidden=True)] = None,
197
225
  ) -> None:
198
226
  """Create a worktree on a new branch and start a supervisor in it."""
@@ -209,6 +237,12 @@ def new(
209
237
  _fail("--base can't be combined with --pr (the PR's base branch is used)")
210
238
  elif not branch:
211
239
  _fail("missing BRANCH (or pass --pr <number>)")
240
+ if not no_agent:
241
+ try:
242
+ root = git.main_repo_root(os.getcwd())
243
+ except git.GitError:
244
+ root = None
245
+ provider = _resolve_supervisor(root, provider, yes=yes)
212
246
  db = DB()
213
247
  if pr is not None:
214
248
  created = _run(workspaces.create_from_pr, db, os.getcwd(), pr, run_setup=not no_setup)
@@ -258,7 +292,9 @@ def start(
258
292
  autopilot: Optional[bool] = typer.Option(None, "--autopilot/--no-autopilot", help="The supervisor drives toward a goal until it's verified (default: on, or `autopilot` in .brindle/config.json)."),
259
293
  branch: Optional[str] = typer.Option(None, "--branch", "-b", help="Run in the worktree for this branch, creating the branch and worktree if needed."),
260
294
  worktree: Optional[str] = typer.Option(None, "--worktree", "-w", help="Run in the worktree at this path (created with --branch if it doesn't exist)."),
295
+ yes: Annotated[bool, typer.Option("--yes", "-y", help="Don't open the supervisor picker: use your last pick, else the default.")] = False,
261
296
  provider_alias: Annotated[Optional[str], typer.Option("--provider", hidden=True)] = None,
297
+ demo: Annotated[bool, typer.Option("--demo-run", hidden=True)] = False,
262
298
  ) -> None:
263
299
  """Start a fresh supervisor chat here, with the dashboard alongside. A launch only ever starts a supervisor; it starts the other agents itself.
264
300
 
@@ -312,6 +348,7 @@ def start(
312
348
  if provider and provider in NOT_SUPERVISOR:
313
349
  _fail(f"the {provider} provider can't run the supervisor; use one of: "
314
350
  f"{', '.join(SUPERVISOR_PROVIDERS)}")
351
+ provider = _resolve_supervisor(ws.repo_root, provider, yes=yes, demo=demo)
315
352
  _preflight(agents.SUPERVISOR, provider, ws.repo_root)
316
353
  _trust_codex_hook(ws.repo_root)
317
354
  # Nothing slow before the chat starts: the paused session's leftover
@@ -366,6 +403,67 @@ def _preflight(agent: str, provider: Optional[str], repo_root: str) -> None:
366
403
  _fail("brindle can't start yet. `brindle doctor` checks everything else.")
367
404
 
368
405
 
406
+ def _is_ci_env() -> bool:
407
+ for key in ("CI", "GITHUB_ACTIONS", "GITLAB_CI", "BUILDKITE", "CIRCLECI", "TF_BUILD", "JENKINS_URL", "BRINDLE_CI"):
408
+ val = (os.environ.get(key) or "").strip().lower()
409
+ if val and val not in ("0", "false", "no", "off"):
410
+ return True
411
+ return False
412
+
413
+
414
+ def _can_prompt(yes: bool = False, demo: bool = False) -> bool:
415
+ return bool(sys.stdin.isatty() and yes is not True and demo is not True and not _is_ci_env())
416
+
417
+
418
+ def _resolve_supervisor(repo_root: str | None, provider: Optional[str], *,
419
+ yes: bool = False, demo: bool = False) -> Optional[str]:
420
+ """The CLI the supervisor runs on (None: its profile's). ``-s CLI`` picks
421
+ it and is remembered; a bare ``-s`` opens the picker. Otherwise the
422
+ person's last pick, asked again in the picker only on the first launch or
423
+ when that pick isn't signed in or allowed, and only where brindle may ask.
424
+ A CLI the org doesn't allow this person is refused, whichever way it came."""
425
+ from brindle import doctor as doctor_mod, picker
426
+ from brindle.config import SUPERVISOR_KEY, set_user, supervisor_pick
427
+ from brindle.pro import team_policy
428
+
429
+ acc = team_policy.access(repo_root)
430
+ pick = supervisor_pick()
431
+
432
+ def ask() -> str:
433
+ chosen = picker.pick_supervisor(acc, pick)
434
+ if chosen is None:
435
+ typer.echo("No supervisor picked; nothing was started.")
436
+ raise typer.Exit(1)
437
+ return chosen
438
+
439
+ if provider == picker.PICK:
440
+ if not _can_prompt(yes, demo):
441
+ _fail("`brindle -s` on its own opens the supervisor picker, which needs a terminal; "
442
+ "name the CLI instead, e.g. `brindle -s codex`")
443
+ provider = ask()
444
+ elif provider:
445
+ why = acc.refusal(provider)
446
+ if why:
447
+ _fail(f"can't run the supervisor on {provider}: {why}")
448
+ if _can_prompt(yes, demo) and provider not in doctor_mod.signed_in_providers():
449
+ if not picker.sign_in(provider):
450
+ raise typer.Exit(1)
451
+ elif _can_prompt(yes, demo) and not (pick and acc.allows(pick)
452
+ and pick in doctor_mod.signed_in_providers()):
453
+ provider = ask()
454
+ elif pick and acc.allows(pick):
455
+ provider = pick
456
+ final = provider or _run(load_profile, agents.SUPERVISOR, repo_root).provider
457
+ why = acc.refusal(final)
458
+ if why:
459
+ _fail(f"can't run the supervisor on {final}: {why}. Pick another with `brindle -s`.")
460
+ if provider:
461
+ if provider != pick:
462
+ set_user(SUPERVISOR_KEY, provider)
463
+ typer.echo(f"Supervisor: {provider} (your pick) · brindle -s to change")
464
+ return provider
465
+
466
+
369
467
  def _trust_codex_hook(repo_root: str) -> None:
370
468
  """With the permission policy on and Codex installed, trust brindle's Codex
371
469
  hook if it isn't yet (or brindle moved), and say so in one line, so Codex
@@ -761,6 +859,7 @@ def default(
761
859
  None, "--supervisor", "-s", help="Run the supervisor on this CLI instead of Claude Code (codex, antigravity). Workers keep their own profiles' providers.",
762
860
  ),
763
861
  show_version: bool = typer.Option(False, "--version", help="Print brindle's version and exit."),
862
+ yes: Annotated[bool, typer.Option("--yes", "-y", help="Don't open the supervisor picker: use your last pick, else the default.")] = False,
764
863
  provider_alias: Annotated[Optional[str], typer.Option("--provider", hidden=True)] = None,
765
864
  ) -> None:
766
865
  """Bare `brindle`: a fresh supervisor chat here (a scratch session outside git)."""
@@ -782,7 +881,7 @@ def default(
782
881
  continue_cmd(session_id=None, attach=True)
783
882
  else:
784
883
  start(prompt=None, provider=provider, attach=True, watch=True,
785
- autopilot=autopilot, branch=None, worktree=None)
884
+ autopilot=autopilot, branch=None, worktree=None, yes=yes)
786
885
 
787
886
 
788
887
  @app.command()
@@ -796,12 +895,13 @@ def supervisor(
796
895
  autopilot: Optional[bool] = typer.Option(None, "--autopilot/--no-autopilot", help="The supervisor drives toward a goal until it's verified (default: on, or `autopilot` in .brindle/config.json)."),
797
896
  branch: Optional[str] = typer.Option(None, "--branch", "-b", help="Run in the worktree for this branch, creating the branch and worktree if needed."),
798
897
  worktree: Optional[str] = typer.Option(None, "--worktree", "-w", help="Run in the worktree at this path (created with --branch if it doesn't exist)."),
898
+ yes: Annotated[bool, typer.Option("--yes", "-y", help="Don't open the supervisor picker: use your last pick, else the default.")] = False,
799
899
  provider_alias: Annotated[Optional[str], typer.Option("--provider", hidden=True)] = None,
800
900
  ) -> None:
801
901
  """Start an explicit supervisor session to coordinate coding agents."""
802
902
  provider = provider or provider_alias
803
903
  start(prompt=prompt, provider=provider, attach=attach, watch=watch,
804
- autopilot=autopilot, branch=branch, worktree=worktree)
904
+ autopilot=autopilot, branch=branch, worktree=worktree, yes=yes)
805
905
 
806
906
 
807
907
  def _session_root(db: DB) -> str:
@@ -267,6 +267,9 @@ def load_repo_config(repo_root: str | Path) -> RepoConfig:
267
267
  if norm_tier in WEIGHTS and isinstance(names, list):
268
268
  cfg.routing[norm_tier] = [n for n in names if isinstance(n, str)]
269
269
  cfg.routing_sources[norm_tier] = str(path)
270
+ from brindle import worker_models
271
+
272
+ worker_models.apply(cfg, user, str(repo_root)) # the models picked on the picker's workers screen
270
273
  for source in (shared, local): # per weight, so a repo can override one and keep the rest
271
274
  second = source.get("second_review")
272
275
  if isinstance(second, dict):
@@ -311,6 +314,16 @@ def user_settings() -> dict:
311
314
  return _read_json(user_config_path())
312
315
 
313
316
 
317
+ SUPERVISOR_KEY = "supervisor" # ~/.brindle/config.json: the CLI the person last picked for the supervisor
318
+
319
+
320
+ def supervisor_pick() -> str | None:
321
+ """The supervisor CLI this person last picked (in the picker or with
322
+ ``-s``), or None before the first pick."""
323
+ pick = user_settings().get(SUPERVISOR_KEY)
324
+ return pick if isinstance(pick, str) and pick else None
325
+
326
+
314
327
  def set_user(key: str, value: object) -> Path:
315
328
  """Set one key in ``~/.brindle/config.json`` (other keys are kept)."""
316
329
  path = user_config_path()
@@ -0,0 +1,66 @@
1
+ """Check a pasted API key with its provider before `brindle keys set` stores it.
2
+ Every check is a read-only GET that generates nothing and costs nothing."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import urllib.error
8
+ import urllib.request
9
+
10
+ # name -> (where to create a key, how the provider is called)
11
+ KEY_PAGES = {
12
+ "ANTHROPIC_API_KEY": "https://console.anthropic.com/settings/keys",
13
+ "OPENAI_API_KEY": "https://platform.openai.com/api-keys",
14
+ "OPENROUTER_API_KEY": "https://openrouter.ai/keys",
15
+ }
16
+
17
+
18
+ def key_page(name: str) -> str | None:
19
+ return KEY_PAGES.get(name)
20
+
21
+
22
+ def personal_keys_denied(repo_root: str | None) -> bool:
23
+ """True when the org's policy sets deny_personal_keys (read as agents.py does)."""
24
+ from brindle import agents
25
+
26
+ m = agents.managed(repo_root or ".")
27
+ return bool(m is not None and m.deny_personal_keys)
28
+
29
+
30
+ def _get(url: str, headers: dict[str, str]) -> tuple[int, dict]:
31
+ req = urllib.request.Request(url, headers=headers, method="GET")
32
+ try:
33
+ with urllib.request.urlopen(req, timeout=15) as resp: # noqa: S310 - fixed https URLs
34
+ return resp.status, json.loads(resp.read() or b"{}")
35
+ except urllib.error.HTTPError as e:
36
+ return e.code, {}
37
+
38
+
39
+ def verify(name: str, value: str) -> str | None:
40
+ """None when the provider accepts the key, else a message saying why not.
41
+ Names without a check are accepted unchecked."""
42
+ if name == "OPENROUTER_API_KEY":
43
+ url, headers = "https://openrouter.ai/api/v1/key", {"Authorization": f"Bearer {value}"}
44
+ elif name == "ANTHROPIC_API_KEY":
45
+ url = "https://api.anthropic.com/v1/models"
46
+ headers = {"x-api-key": value, "anthropic-version": "2023-06-01"}
47
+ elif name == "OPENAI_API_KEY":
48
+ url, headers = "https://api.openai.com/v1/models", {"Authorization": f"Bearer {value}"}
49
+ else:
50
+ return None
51
+ try:
52
+ status, body = _get(url, headers)
53
+ except (OSError, ValueError) as e:
54
+ return f"couldn't reach the provider to check the key: {e}"
55
+ if status == 401:
56
+ return "the provider rejected this key (401 unauthorized); check that you copied all of it"
57
+ if status != 200:
58
+ return f"the provider answered {status} when checking the key"
59
+ if name == "OPENROUTER_API_KEY":
60
+ data = (body.get("data") if isinstance(body, dict) else None) or {}
61
+ if not isinstance(data, dict):
62
+ data = {}
63
+ left = data.get("limit_remaining")
64
+ if left is not None and left <= 0:
65
+ return "this OpenRouter key has no credit left"
66
+ return None
@@ -7,10 +7,11 @@ from __future__ import annotations
7
7
  import getpass
8
8
  import os
9
9
  import sys
10
+ import webbrowser
10
11
 
11
12
  import typer
12
13
 
13
- from brindle import keystore
14
+ from brindle import key_check, keystore
14
15
 
15
16
  app = typer.Typer(no_args_is_help=True,
16
17
  help="Store model API keys once; every agent pane gets the ones its provider reads.")
@@ -37,10 +38,26 @@ def keys_set(name: str = typer.Argument(..., help="The variable, e.g. ANTHROPIC_
37
38
  keystore.check_name(name, _root())
38
39
  except keystore.CredentialError as e:
39
40
  _fail(str(e))
41
+ try:
42
+ denied = key_check.personal_keys_denied(_root())
43
+ except Exception as e: # noqa: BLE001 - policy unreadable: fail closed
44
+ _fail(f"couldn't read your org's key policy: {e}")
45
+ if denied:
46
+ _fail("your org's policy denies personal API keys; brindle won't store one")
47
+ page = key_check.key_page(name)
40
48
  if sys.stdin.isatty():
49
+ if page:
50
+ typer.echo(f"Get a key at {page}")
51
+ if input("Press o to open it in your browser, or Enter to paste a key: ").strip().lower() == "o":
52
+ webbrowser.open(page)
41
53
  value = getpass.getpass(f"{name}: ")
42
54
  else:
43
55
  value = sys.stdin.readline()
56
+ value = value.strip()
57
+ if value:
58
+ problem = key_check.verify(name, value)
59
+ if problem:
60
+ _fail(f"{name} not stored: {problem}")
44
61
  try:
45
62
  keystore.set_key(name, value, _root())
46
63
  except keystore.CredentialError as e: