brindle 0.2.1__tar.gz → 0.3.0__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.
- {brindle-0.2.1 → brindle-0.3.0}/PKG-INFO +51 -11
- {brindle-0.2.1 → brindle-0.3.0}/README.md +50 -10
- {brindle-0.2.1 → brindle-0.3.0}/pyproject.toml +3 -1
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/agents.py +63 -10
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/autopilot.py +5 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/builtin_agents/developer-light.md +2 -2
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/ci_adapters.py +83 -11
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/ci_client.py +368 -13
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/ci_federation.py +250 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/ci_hosts.py +65 -6
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/cli.py +53 -5
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/config.py +6 -2
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/cost.py +12 -1
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/cull.py +61 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/doctor.py +80 -0
- brindle-0.3.0/src/brindle/keys_cmds.py +103 -0
- brindle-0.2.1/src/brindle/pro/credentials.py → brindle-0.3.0/src/brindle/keystore.py +145 -39
- brindle-0.3.0/src/brindle/model_access.py +293 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/native/runner.py +4 -1
- brindle-0.3.0/src/brindle/pane_auth.py +56 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/pipeline.py +70 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/pool.py +15 -11
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/pricing.py +2 -1
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/pro/account.py +68 -8
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/pro/auth.py +29 -6
- brindle-0.3.0/src/brindle/pro/credentials.py +53 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/pro/managed_models.py +53 -3
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/pro/org_profiles.py +33 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/pro/team_policy.py +39 -3
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/procs.py +23 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/profiles.py +33 -2
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/providers.py +162 -9
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/quota.py +14 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/secrets.py +54 -8
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/workspaces.py +27 -7
- {brindle-0.2.1 → brindle-0.3.0}/.gitignore +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/LICENSE +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/SCHEDULE-A +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/__init__.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/__main__.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/account.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/airgap.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/antigravity.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/budget.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/builtin_agents/__init__.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/builtin_agents/backend-auditor.md +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/builtin_agents/developer-codex.md +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/builtin_agents/developer-heavy.md +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/builtin_agents/developer-local.md +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/builtin_agents/developer.md +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/builtin_agents/qa.md +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/builtin_agents/reviewer-codex.md +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/builtin_agents/reviewer-local.md +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/builtin_agents/reviewer.md +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/builtin_agents/subagent.md +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/builtin_agents/supervisor.md +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/builtin_rules/__init__.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/builtin_rules/security/backend.md +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/builtin_rules/style/minimal-diff.md +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/builtin_rules/tests/only.md +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/codemap.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/codex_hook.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/conflicts.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/cost_estimate.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/db.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/demo.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/detect.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/escapes.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/events.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/gates.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/git.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/guardrails.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/history.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/inbox.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/learned_rules.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/learning.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/legacy.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/mcp_server.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/native/__init__.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/native/client.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/native/loop.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/native/permissions.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/native/serve.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/native/tools.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/permissions.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/plugins.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/policy.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/pro/__init__.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/pro/_files.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/pro/audit_chain.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/pro/audit_export.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/pro/brief_learning.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/pro/cost_centers.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/pro/features.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/pro/keys.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/pro/learning.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/pro/license.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/pro/loopback.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/pro/orgkey.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/pro/rollout.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/pro/settings_sync.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/pro/status.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/pro/team_events.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/repo_cmds.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/repos.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/rewind.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/rule_checks.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/savings.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/scratch.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/services.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/sessions.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/sidebar_follow.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/status_cache.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/tasks.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/tmux.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/usage.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/view.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/watch.py +0 -0
- {brindle-0.2.1 → brindle-0.3.0}/src/brindle/watch_costs.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: brindle
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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.
|
|
92
|
+
session shows it too (`brindle 0.3.0`). 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
|
|
@@ -350,6 +350,7 @@ your own status line prints, so what you see doesn't change.
|
|
|
350
350
|
| `brindle transfer [REPO] [--from SESSION] [-b BRANCH]` | move a scratch session's work into a real repo |
|
|
351
351
|
| `brindle ls [--all]` | workspaces and agents |
|
|
352
352
|
| `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") |
|
|
353
|
+
| `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 |
|
|
353
354
|
| `brindle history [--limit N] [--kind K] [--all]` | durable log of worker results, reviews, merges and milestone checks |
|
|
354
355
|
| `brindle cost [--days N] [--all]` | dollar spend at list prices over the last 30 days, by model |
|
|
355
356
|
| `brindle cost report [--days N] [--all]` | Pro: spend by day, profile and goal, cost per merged branch, review pass rate per worker profile |
|
|
@@ -361,7 +362,7 @@ your own status line prints, so what you see doesn't change.
|
|
|
361
362
|
| `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) |
|
|
362
363
|
| `brindle learning` | whether brindle Pro's hosted learning is on for this repo, and if not, why (nothing is learned on your machine) |
|
|
363
364
|
| `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 |
|
|
364
|
-
| `brindle account [login\|logout\|status\|upgrade\|portal\|org]` | paid features: bare `brindle account` shows what your plan has and how to get the rest (see "brindle Pro and Team" below) |
|
|
365
|
+
| `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) |
|
|
365
366
|
| `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) |
|
|
366
367
|
| `brindle watch [--all] [--once]` | the dashboard on its own (the same view as the sidebar): enter attaches, `p` peeks, `x` closes |
|
|
367
368
|
| `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 |
|
|
@@ -526,6 +527,7 @@ Autopilot, merge gates and cleanup:
|
|
|
526
527
|
| `max_agents` | `4` | workers running at once per session (`0`: no cap) |
|
|
527
528
|
| `check_timeout` | `900` | seconds each check may take |
|
|
528
529
|
| `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 |
|
|
530
|
+
| `fable_escalation` | on, unless the worker runs on a cloud account (Bedrock, Vertex or Foundry) | retry a failed heavy task once on `developer-heavy` (Fable) before handing it to you; `true` turns it on for a cloud account, `false` off |
|
|
529
531
|
| `usage_limit` | `90` | autopilot stops pushing on at this % of your Claude usage limit |
|
|
530
532
|
| `limit_cooldown_minutes` | `300` for Antigravity | how long a provider that hit its limit counts as unavailable |
|
|
531
533
|
| `graphify` | if the graph is there | point agents at the repo's [graphify](https://github.com/safishamsi/graphify) code map (`false` turns it off) |
|
|
@@ -589,9 +591,9 @@ keep theirs):
|
|
|
589
591
|
```json
|
|
590
592
|
{
|
|
591
593
|
"routing": {
|
|
592
|
-
"light": ["developer", "developer-codex", "developer-local"],
|
|
594
|
+
"light": ["developer-light", "developer", "developer-codex", "developer-local"],
|
|
593
595
|
"medium": ["developer", "developer-codex", "developer-heavy"],
|
|
594
|
-
"heavy": ["developer
|
|
596
|
+
"heavy": ["developer", "developer-heavy", "developer-codex"]
|
|
595
597
|
}
|
|
596
598
|
}
|
|
597
599
|
```
|
|
@@ -599,7 +601,9 @@ keep theirs):
|
|
|
599
601
|
No tier prefers another provider for its own sake: all-Claude or all-Codex may
|
|
600
602
|
be what works best for you, and that's for the learner to find out.
|
|
601
603
|
|
|
602
|
-
`developer-
|
|
604
|
+
`developer-light` runs on Claude Haiku at low effort; `developer` on your Claude
|
|
605
|
+
Code default model; `developer-codex` on Codex; `developer-heavy` on Claude Fable
|
|
606
|
+
at high effort.
|
|
603
607
|
A profile is skipped when its CLI isn't installed (`claude`, `codex`, `agy`), the
|
|
604
608
|
local model server isn't answering, or its provider is at your `usage_limit`. If
|
|
605
609
|
hosted learning is on it chooses among the profiles left; otherwise the first
|
|
@@ -762,7 +766,9 @@ brindle account login # opens your browser to sign in (--device: enter a co
|
|
|
762
766
|
brindle account status # your plan, features, hosted learning on or off, when the entitlement expires
|
|
763
767
|
brindle account savings # what hosted learning's picks gained in this repo, this month and last: estimates, from this machine's records only
|
|
764
768
|
brindle account upgrade # opens the checkout for brindle Pro (and prints its URL)
|
|
765
|
-
brindle account
|
|
769
|
+
brindle account upgrade --enterprise [--trial] --seats N --org ORG # brindle Enterprise for a team org; --trial: free trial, at most 10 seats
|
|
770
|
+
brindle account portal # opens the billing portal (invoices, payment method, cancellation); --org ORG for a team org
|
|
771
|
+
brindle account seats N # set a Team or Enterprise org's seat count (billing admin); --org ORG; Stripe prorates the change
|
|
766
772
|
brindle account org list # the orgs you belong to; `org use <id>` switches, `org policy` shows the current one
|
|
767
773
|
brindle account org policy # the org's policy, its per-role overrides, and the policy that applies to you
|
|
768
774
|
brindle account org profiles # the org's shared profiles and rule packs, and which are pinned
|
|
@@ -825,6 +831,16 @@ entitlement.
|
|
|
825
831
|
* `deny_personal_keys`: the personal model keys (`ANTHROPIC_API_KEY`,
|
|
826
832
|
`OPENAI_API_KEY`, ...) are taken out of every agent pane, and a profile that
|
|
827
833
|
sets one is refused.
|
|
834
|
+
* An org-owned key: with `provider` `anthropic` (or `openai-compatible`), set
|
|
835
|
+
`key_env` to the name of a variable that holds the org's key (not a personal
|
|
836
|
+
one like `ANTHROPIC_API_KEY`), or `key_helper` to a command, as a list of
|
|
837
|
+
arguments, that prints the key (for example `["vault", "kv", "get",
|
|
838
|
+
"-field=key", "secret/anthropic"]`). brindle runs the helper on the person's
|
|
839
|
+
machine, without a shell and with a timeout, each time a worker starts, and
|
|
840
|
+
gives the key to that worker as `ANTHROPIC_API_KEY` (`OPENAI_API_KEY` for
|
|
841
|
+
Codex). It passes `deny_personal_keys`; a personal key doesn't. The key never
|
|
842
|
+
goes through brindle's servers. When the variable is missing or the helper
|
|
843
|
+
fails, the worker doesn't start.
|
|
828
844
|
|
|
829
845
|
Two things to know when you turn on `deny_personal_keys`:
|
|
830
846
|
|
|
@@ -985,17 +1001,31 @@ workflows call these commands:
|
|
|
985
1001
|
commits the workflows on a `brindle/ci-setup` branch, then puts you back on
|
|
986
1002
|
the branch you started on and deletes the local setup branch, so `git pull`
|
|
987
1003
|
after merging the setup pull request is clean (commit or stash your changes
|
|
988
|
-
first: it refuses to start with uncommitted work).
|
|
1004
|
+
first: it refuses to start with uncommitted work). With your own cloud
|
|
1005
|
+
(`--credential bedrock|vertex|foundry`, keyless GitHub OIDC) it can read the
|
|
1006
|
+
IDs from the deployment template's outputs instead of asking:
|
|
1007
|
+
`--from-stack NAME [--region R]` (AWS CloudFormation), `--from-terraform DIR`
|
|
1008
|
+
(Google) or `--from-deployment NAME -g RG` (Azure). They are defaults for
|
|
1009
|
+
`--cloud-var`, which still overrides them, and every ID is format-checked. A
|
|
1010
|
+
missing `aws`, `terraform` or `az` CLI or output is an error that names the
|
|
1011
|
+
`--cloud-var` to pass instead. Vertex AI and Microsoft Foundry are in
|
|
1012
|
+
**preview** (not yet verified end to end): `init` refuses them unless you pass
|
|
1013
|
+
`--preview` or set `BRINDLE_CI_PREVIEW_CLOUDS=1`, and `doctor` marks them
|
|
1014
|
+
`(preview)`. Bedrock is generally available.
|
|
989
1015
|
- `brindle ci doctor`: which provider CLIs and credential names a runner has, and
|
|
990
|
-
which providers CI may use on this repository.
|
|
1016
|
+
which providers CI may use on this repository. `--models` also makes one tiny
|
|
1017
|
+
call per pinned model and says why any is refused (a quota, a form not filled in).
|
|
991
1018
|
- `brindle ci start`: start a run for an issue, a goal text or a dispatched run,
|
|
992
1019
|
or a validation of a pull request.
|
|
993
1020
|
- `brindle ci run`: run what was started: the supervisor and the result upload,
|
|
994
1021
|
or the pull request's checks and reviews and the evidence upload.
|
|
995
1022
|
- `brindle ci report`: tell the service how the workflow's jobs ended.
|
|
996
1023
|
|
|
997
|
-
Credential names are reported, never values.
|
|
998
|
-
|
|
1024
|
+
Credential names are reported, never values. CI uses API keys, cloud sign-ins
|
|
1025
|
+
(Bedrock, Vertex, Foundry) or identity federation only: a provider signed in
|
|
1026
|
+
only with a subscription (`CLAUDE_CODE_OAUTH_TOKEN`, a Codex ChatGPT login) is
|
|
1027
|
+
not used, on personal and organization repositories alike. Add an API key
|
|
1028
|
+
secret instead.
|
|
999
1029
|
|
|
1000
1030
|
Fix builds only fix the default branch's *required* status checks, and a new
|
|
1001
1031
|
repository has none. When the default branch requires none, `brindle ci init`
|
|
@@ -1240,6 +1270,16 @@ needs no browser step. Put the key in your environment, or in a profile's
|
|
|
1240
1270
|
`brindle doctor` names the variable each CLI gets, from your shell or from
|
|
1241
1271
|
which profiles.
|
|
1242
1272
|
|
|
1273
|
+
A profile chooses between the two with `auth`:
|
|
1274
|
+
|
|
1275
|
+
| `auth:` | What the worker signs in with |
|
|
1276
|
+
|---|---|
|
|
1277
|
+
| `auto` (default) | A key from your environment or `brindle keys` if there is one, else the CLI's own login. |
|
|
1278
|
+
| `subscription` | Always the CLI's own login: brindle strips that provider's API keys from the pane. |
|
|
1279
|
+
| `api_key` | Only a key: the worker refuses to start without one. |
|
|
1280
|
+
|
|
1281
|
+
Your org's `deny_personal_keys` (Enterprise) still applies on top of `auth`.
|
|
1282
|
+
|
|
1243
1283
|
### Cheap workers
|
|
1244
1284
|
|
|
1245
1285
|
By default a Claude Code worker loads everything your own `claude` does: your
|
|
@@ -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.
|
|
62
|
+
session shows it too (`brindle 0.3.0`). 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
|
|
@@ -320,6 +320,7 @@ your own status line prints, so what you see doesn't change.
|
|
|
320
320
|
| `brindle transfer [REPO] [--from SESSION] [-b BRANCH]` | move a scratch session's work into a real repo |
|
|
321
321
|
| `brindle ls [--all]` | workspaces and agents |
|
|
322
322
|
| `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") |
|
|
323
|
+
| `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 |
|
|
323
324
|
| `brindle history [--limit N] [--kind K] [--all]` | durable log of worker results, reviews, merges and milestone checks |
|
|
324
325
|
| `brindle cost [--days N] [--all]` | dollar spend at list prices over the last 30 days, by model |
|
|
325
326
|
| `brindle cost report [--days N] [--all]` | Pro: spend by day, profile and goal, cost per merged branch, review pass rate per worker profile |
|
|
@@ -331,7 +332,7 @@ your own status line prints, so what you see doesn't change.
|
|
|
331
332
|
| `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) |
|
|
332
333
|
| `brindle learning` | whether brindle Pro's hosted learning is on for this repo, and if not, why (nothing is learned on your machine) |
|
|
333
334
|
| `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 |
|
|
334
|
-
| `brindle account [login\|logout\|status\|upgrade\|portal\|org]` | paid features: bare `brindle account` shows what your plan has and how to get the rest (see "brindle Pro and Team" below) |
|
|
335
|
+
| `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) |
|
|
335
336
|
| `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) |
|
|
336
337
|
| `brindle watch [--all] [--once]` | the dashboard on its own (the same view as the sidebar): enter attaches, `p` peeks, `x` closes |
|
|
337
338
|
| `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 |
|
|
@@ -496,6 +497,7 @@ Autopilot, merge gates and cleanup:
|
|
|
496
497
|
| `max_agents` | `4` | workers running at once per session (`0`: no cap) |
|
|
497
498
|
| `check_timeout` | `900` | seconds each check may take |
|
|
498
499
|
| `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 |
|
|
500
|
+
| `fable_escalation` | on, unless the worker runs on a cloud account (Bedrock, Vertex or Foundry) | retry a failed heavy task once on `developer-heavy` (Fable) before handing it to you; `true` turns it on for a cloud account, `false` off |
|
|
499
501
|
| `usage_limit` | `90` | autopilot stops pushing on at this % of your Claude usage limit |
|
|
500
502
|
| `limit_cooldown_minutes` | `300` for Antigravity | how long a provider that hit its limit counts as unavailable |
|
|
501
503
|
| `graphify` | if the graph is there | point agents at the repo's [graphify](https://github.com/safishamsi/graphify) code map (`false` turns it off) |
|
|
@@ -559,9 +561,9 @@ keep theirs):
|
|
|
559
561
|
```json
|
|
560
562
|
{
|
|
561
563
|
"routing": {
|
|
562
|
-
"light": ["developer", "developer-codex", "developer-local"],
|
|
564
|
+
"light": ["developer-light", "developer", "developer-codex", "developer-local"],
|
|
563
565
|
"medium": ["developer", "developer-codex", "developer-heavy"],
|
|
564
|
-
"heavy": ["developer
|
|
566
|
+
"heavy": ["developer", "developer-heavy", "developer-codex"]
|
|
565
567
|
}
|
|
566
568
|
}
|
|
567
569
|
```
|
|
@@ -569,7 +571,9 @@ keep theirs):
|
|
|
569
571
|
No tier prefers another provider for its own sake: all-Claude or all-Codex may
|
|
570
572
|
be what works best for you, and that's for the learner to find out.
|
|
571
573
|
|
|
572
|
-
`developer-
|
|
574
|
+
`developer-light` runs on Claude Haiku at low effort; `developer` on your Claude
|
|
575
|
+
Code default model; `developer-codex` on Codex; `developer-heavy` on Claude Fable
|
|
576
|
+
at high effort.
|
|
573
577
|
A profile is skipped when its CLI isn't installed (`claude`, `codex`, `agy`), the
|
|
574
578
|
local model server isn't answering, or its provider is at your `usage_limit`. If
|
|
575
579
|
hosted learning is on it chooses among the profiles left; otherwise the first
|
|
@@ -732,7 +736,9 @@ brindle account login # opens your browser to sign in (--device: enter a co
|
|
|
732
736
|
brindle account status # your plan, features, hosted learning on or off, when the entitlement expires
|
|
733
737
|
brindle account savings # what hosted learning's picks gained in this repo, this month and last: estimates, from this machine's records only
|
|
734
738
|
brindle account upgrade # opens the checkout for brindle Pro (and prints its URL)
|
|
735
|
-
brindle account
|
|
739
|
+
brindle account upgrade --enterprise [--trial] --seats N --org ORG # brindle Enterprise for a team org; --trial: free trial, at most 10 seats
|
|
740
|
+
brindle account portal # opens the billing portal (invoices, payment method, cancellation); --org ORG for a team org
|
|
741
|
+
brindle account seats N # set a Team or Enterprise org's seat count (billing admin); --org ORG; Stripe prorates the change
|
|
736
742
|
brindle account org list # the orgs you belong to; `org use <id>` switches, `org policy` shows the current one
|
|
737
743
|
brindle account org policy # the org's policy, its per-role overrides, and the policy that applies to you
|
|
738
744
|
brindle account org profiles # the org's shared profiles and rule packs, and which are pinned
|
|
@@ -795,6 +801,16 @@ entitlement.
|
|
|
795
801
|
* `deny_personal_keys`: the personal model keys (`ANTHROPIC_API_KEY`,
|
|
796
802
|
`OPENAI_API_KEY`, ...) are taken out of every agent pane, and a profile that
|
|
797
803
|
sets one is refused.
|
|
804
|
+
* An org-owned key: with `provider` `anthropic` (or `openai-compatible`), set
|
|
805
|
+
`key_env` to the name of a variable that holds the org's key (not a personal
|
|
806
|
+
one like `ANTHROPIC_API_KEY`), or `key_helper` to a command, as a list of
|
|
807
|
+
arguments, that prints the key (for example `["vault", "kv", "get",
|
|
808
|
+
"-field=key", "secret/anthropic"]`). brindle runs the helper on the person's
|
|
809
|
+
machine, without a shell and with a timeout, each time a worker starts, and
|
|
810
|
+
gives the key to that worker as `ANTHROPIC_API_KEY` (`OPENAI_API_KEY` for
|
|
811
|
+
Codex). It passes `deny_personal_keys`; a personal key doesn't. The key never
|
|
812
|
+
goes through brindle's servers. When the variable is missing or the helper
|
|
813
|
+
fails, the worker doesn't start.
|
|
798
814
|
|
|
799
815
|
Two things to know when you turn on `deny_personal_keys`:
|
|
800
816
|
|
|
@@ -955,17 +971,31 @@ workflows call these commands:
|
|
|
955
971
|
commits the workflows on a `brindle/ci-setup` branch, then puts you back on
|
|
956
972
|
the branch you started on and deletes the local setup branch, so `git pull`
|
|
957
973
|
after merging the setup pull request is clean (commit or stash your changes
|
|
958
|
-
first: it refuses to start with uncommitted work).
|
|
974
|
+
first: it refuses to start with uncommitted work). With your own cloud
|
|
975
|
+
(`--credential bedrock|vertex|foundry`, keyless GitHub OIDC) it can read the
|
|
976
|
+
IDs from the deployment template's outputs instead of asking:
|
|
977
|
+
`--from-stack NAME [--region R]` (AWS CloudFormation), `--from-terraform DIR`
|
|
978
|
+
(Google) or `--from-deployment NAME -g RG` (Azure). They are defaults for
|
|
979
|
+
`--cloud-var`, which still overrides them, and every ID is format-checked. A
|
|
980
|
+
missing `aws`, `terraform` or `az` CLI or output is an error that names the
|
|
981
|
+
`--cloud-var` to pass instead. Vertex AI and Microsoft Foundry are in
|
|
982
|
+
**preview** (not yet verified end to end): `init` refuses them unless you pass
|
|
983
|
+
`--preview` or set `BRINDLE_CI_PREVIEW_CLOUDS=1`, and `doctor` marks them
|
|
984
|
+
`(preview)`. Bedrock is generally available.
|
|
959
985
|
- `brindle ci doctor`: which provider CLIs and credential names a runner has, and
|
|
960
|
-
which providers CI may use on this repository.
|
|
986
|
+
which providers CI may use on this repository. `--models` also makes one tiny
|
|
987
|
+
call per pinned model and says why any is refused (a quota, a form not filled in).
|
|
961
988
|
- `brindle ci start`: start a run for an issue, a goal text or a dispatched run,
|
|
962
989
|
or a validation of a pull request.
|
|
963
990
|
- `brindle ci run`: run what was started: the supervisor and the result upload,
|
|
964
991
|
or the pull request's checks and reviews and the evidence upload.
|
|
965
992
|
- `brindle ci report`: tell the service how the workflow's jobs ended.
|
|
966
993
|
|
|
967
|
-
Credential names are reported, never values.
|
|
968
|
-
|
|
994
|
+
Credential names are reported, never values. CI uses API keys, cloud sign-ins
|
|
995
|
+
(Bedrock, Vertex, Foundry) or identity federation only: a provider signed in
|
|
996
|
+
only with a subscription (`CLAUDE_CODE_OAUTH_TOKEN`, a Codex ChatGPT login) is
|
|
997
|
+
not used, on personal and organization repositories alike. Add an API key
|
|
998
|
+
secret instead.
|
|
969
999
|
|
|
970
1000
|
Fix builds only fix the default branch's *required* status checks, and a new
|
|
971
1001
|
repository has none. When the default branch requires none, `brindle ci init`
|
|
@@ -1210,6 +1240,16 @@ needs no browser step. Put the key in your environment, or in a profile's
|
|
|
1210
1240
|
`brindle doctor` names the variable each CLI gets, from your shell or from
|
|
1211
1241
|
which profiles.
|
|
1212
1242
|
|
|
1243
|
+
A profile chooses between the two with `auth`:
|
|
1244
|
+
|
|
1245
|
+
| `auth:` | What the worker signs in with |
|
|
1246
|
+
|---|---|
|
|
1247
|
+
| `auto` (default) | A key from your environment or `brindle keys` if there is one, else the CLI's own login. |
|
|
1248
|
+
| `subscription` | Always the CLI's own login: brindle strips that provider's API keys from the pane. |
|
|
1249
|
+
| `api_key` | Only a key: the worker refuses to start without one. |
|
|
1250
|
+
|
|
1251
|
+
Your org's `deny_personal_keys` (Enterprise) still applies on top of `auth`.
|
|
1252
|
+
|
|
1213
1253
|
### Cheap workers
|
|
1214
1254
|
|
|
1215
1255
|
By default a Claude Code worker loads everything your own `claude` does: your
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "brindle"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
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"
|
|
@@ -52,6 +52,8 @@ audit_export = "brindle.pro.audit_export:make"
|
|
|
52
52
|
dev = [
|
|
53
53
|
"pytest>=8",
|
|
54
54
|
"pytest-xdist>=3.8.0",
|
|
55
|
+
"python-hcl2>=8.1.4",
|
|
56
|
+
"pyyaml>=6.0.3",
|
|
55
57
|
]
|
|
56
58
|
|
|
57
59
|
[build-system]
|
|
@@ -20,7 +20,7 @@ import sys
|
|
|
20
20
|
import time
|
|
21
21
|
import uuid
|
|
22
22
|
|
|
23
|
-
from brindle import git, secrets, tmux, workspaces
|
|
23
|
+
from brindle import git, keystore, pane_auth, providers, secrets, tmux, workspaces
|
|
24
24
|
from brindle.config import RepoConfig
|
|
25
25
|
from brindle.db import DB, Agent, Workspace
|
|
26
26
|
from brindle.profiles import load_profile, missing_add_dirs
|
|
@@ -180,7 +180,10 @@ def agent_env(ws: Workspace, agent_id: str, agent: Agent | None = None,
|
|
|
180
180
|
profile_env = {}
|
|
181
181
|
if m is _UNREAD:
|
|
182
182
|
m = managed(ws.repo_root)
|
|
183
|
-
|
|
183
|
+
# Claude Code's own managed settings (apiKeyHelper, a managed env) pick
|
|
184
|
+
# the credential and backend: brindle adds no provider_config env there.
|
|
185
|
+
org = agent.provider == "claude" and providers.claude_org_managed()
|
|
186
|
+
env = {**profile_env, **({} if org else managed_models.agent_env(m, agent.provider)), **env}
|
|
184
187
|
if agent is not None and preload_tools(agent, ws):
|
|
185
188
|
# Claude Code defers MCP tools and loads them on demand, which costs a
|
|
186
189
|
# worker an extra round trip at the moment it's told to report (and
|
|
@@ -419,11 +422,33 @@ def _open_window(db: DB, agent: Agent, ws: Workspace, name: str, argv: list[str]
|
|
|
419
422
|
# reach the pane, kept or not.
|
|
420
423
|
if m is _UNREAD:
|
|
421
424
|
m = managed(ws.repo_root)
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
425
|
+
env = agent_env(ws, agent.id, agent, m)
|
|
426
|
+
credentials = secrets.agent_credentials(agent.provider, agent.profile, ws.repo_root)
|
|
427
|
+
# Keys stored with `brindle keys` reach this window's env only (never the
|
|
428
|
+
# tmux server's), unless the environment already has the name or an org's
|
|
429
|
+
# deny_personal_keys keeps it out. deny/allow strip it as they do an exported key.
|
|
430
|
+
# The profile's `auth` (subscription strips the provider's keys) and Claude
|
|
431
|
+
# Code's managed settings (a personal key mustn't bypass them) add to the
|
|
432
|
+
# org's denied keys; the org's own key (key_env / key_helper) is set last and
|
|
433
|
+
# passes the deny (secrets.org_key), a personal key under its name doesn't.
|
|
434
|
+
from brindle.pro import managed_models
|
|
435
|
+
|
|
436
|
+
org_managed = agent.provider == "claude" and bool(providers.claude_org_managed())
|
|
437
|
+
deny = pane_auth.deny_names(agent.provider, guarded.auth if guarded is not None else "auto",
|
|
438
|
+
m, org_managed)
|
|
439
|
+
try:
|
|
440
|
+
org_env = managed_models.org_key_env(m, agent.provider)
|
|
441
|
+
except managed_models.ManagedUnavailable as e:
|
|
442
|
+
raise AgentError(f"managed models: {e}") from None
|
|
443
|
+
# Claude Code's managed settings supply the credential: no stored Claude key.
|
|
444
|
+
no_stored = set(deny) | (set(providers._ENV_AUTH["claude"]) if org_managed else set())
|
|
445
|
+
env = {**keystore.pane_keys(credentials - no_stored, {**env, **org_env}), **env, **org_env}
|
|
446
|
+
with secrets.org_key(org_env):
|
|
447
|
+
target = tmux.new_window(ws.tmux_session, name, ws.path, argv, env,
|
|
448
|
+
tag=(AGENT_TAG, agent.id),
|
|
449
|
+
keep=credentials,
|
|
450
|
+
**({"allow": allow} if allow is not None else {}),
|
|
451
|
+
**({"deny": deny} if deny else {}))
|
|
427
452
|
# The pane id only means anything on the server that issued it (a
|
|
428
453
|
# resumed agent may come back on a different one), so record both.
|
|
429
454
|
server = tmux.current_server()
|
|
@@ -591,9 +616,16 @@ def _launch(db: DB, agent: Agent, ws: Workspace, *, prompt: str | None,
|
|
|
591
616
|
# no personal keys (blank, so the launcher's own copy doesn't count).
|
|
592
617
|
from brindle.pro import managed_models
|
|
593
618
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
619
|
+
launch_env = {**profile.env, **managed_models.agent_env(m, provider.name)}
|
|
620
|
+
deny = pane_auth.deny_names(provider.name, profile.auth, m,
|
|
621
|
+
provider.name == "claude" and bool(providers.claude_org_managed()))
|
|
622
|
+
org_key = managed_models.has_org_key(m, provider.name)
|
|
623
|
+
why = pane_auth.key_problem(profile, provider.name, deny, org_key)
|
|
624
|
+
if why:
|
|
625
|
+
raise AgentError(why)
|
|
626
|
+
# The org's own key signs the agent in: there is no login to check.
|
|
627
|
+
why = None if org_key else signed_out(
|
|
628
|
+
provider.name, {**launch_env, **{k: "" for k in secrets.pane_deny(deny, launch_env)}})
|
|
597
629
|
if why:
|
|
598
630
|
raise AgentError(why)
|
|
599
631
|
# Here rather than in spawn, so a resume checks too: a directory can be
|
|
@@ -2319,6 +2351,22 @@ def agent_for_session(db: DB, session_id: object) -> str | None:
|
|
|
2319
2351
|
return matches[-1].id if matches else None
|
|
2320
2352
|
|
|
2321
2353
|
|
|
2354
|
+
def command_running(db: DB, agent: Agent) -> bool:
|
|
2355
|
+
"""Whether a command the agent started (possibly in the background) is
|
|
2356
|
+
still running under it. Never raises: when unsure, it says no."""
|
|
2357
|
+
if agent.provider != "claude":
|
|
2358
|
+
return False
|
|
2359
|
+
try:
|
|
2360
|
+
from brindle import procs
|
|
2361
|
+
|
|
2362
|
+
window = agent.tmux_window if same_server(agent) and owns_pane(db, agent) else ""
|
|
2363
|
+
pane_pids = tmux.window_pids(window) if window else []
|
|
2364
|
+
return bool(procs.running_commands(agent.id, pane_pids))
|
|
2365
|
+
except Exception: # noqa: BLE001
|
|
2366
|
+
log.exception("brindle: couldn't look for %s's running commands", agent.id)
|
|
2367
|
+
return False
|
|
2368
|
+
|
|
2369
|
+
|
|
2322
2370
|
def tell_parent_unreported(db: DB, agent: Agent) -> None:
|
|
2323
2371
|
"""A worker stopped again after being reminded to report, still without a
|
|
2324
2372
|
result. It won't be reminded again on its own, and its supervisor has
|
|
@@ -2336,6 +2384,11 @@ def tell_parent_unreported(db: DB, agent: Agent) -> None:
|
|
|
2336
2384
|
return # its verdict for the current commit is already recorded
|
|
2337
2385
|
if agent.unreported_noted:
|
|
2338
2386
|
return # the supervisor was told once; no repeats
|
|
2387
|
+
if command_running(db, agent):
|
|
2388
|
+
# Its turn ended while a long or background command (a test run) is
|
|
2389
|
+
# still going: it isn't stopped, and the command finishing wakes it.
|
|
2390
|
+
# Not marked noted, so a real stop later still gets its one alert.
|
|
2391
|
+
return
|
|
2339
2392
|
db.update_agent(agent.id, unreported_noted=1)
|
|
2340
2393
|
where = f" (branch `{ws.branch}`, workspace {ws.id})" if ws else ""
|
|
2341
2394
|
tool = "submit_review" if agent.mode == "review" else "report_result"
|
|
@@ -451,6 +451,11 @@ def _unavailable(name: str, cfg: RepoConfig, repo_root: str, gate=None,
|
|
|
451
451
|
over = gate.why_not(name, weight) if gate is not None and gate.active else None
|
|
452
452
|
if over:
|
|
453
453
|
return f"over budget, skipped {name} ({over})"
|
|
454
|
+
from brindle import model_access
|
|
455
|
+
|
|
456
|
+
denied = model_access.refused(name, repo_root)
|
|
457
|
+
if denied:
|
|
458
|
+
return f"model refused for this account ({denied.cause}; {denied.fix}), skipped {name}"
|
|
454
459
|
cli = CLI_FOR_PROVIDER.get(p.provider)
|
|
455
460
|
if cli and unusable(p.provider, p.env):
|
|
456
461
|
return (f"{cli} isn't signed in, skipped {name}" if signed_out(p.provider, p.env)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: developer-light
|
|
3
|
-
description: Implements a small, well-specified task on its own branch, on
|
|
3
|
+
description: Implements a small, well-specified task on its own branch, on Haiku with low effort (the baseline of the light tier of weight routing)
|
|
4
4
|
extends: developer
|
|
5
|
-
model:
|
|
5
|
+
model: haiku
|
|
6
6
|
effort: low
|
|
7
7
|
---
|
|
@@ -19,11 +19,12 @@ An adapter answers four questions and does three things:
|
|
|
19
19
|
its raw reply.
|
|
20
20
|
* ``usage``: token usage of a session, by model.
|
|
21
21
|
|
|
22
|
-
The credential rule (:func:`usable`):
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
(:func:`repo_is_org`)
|
|
22
|
+
The credential rule (:func:`usable`): CI uses API keys, cloud sign-ins or
|
|
23
|
+
identity federation only. A provider whose only credential is a personal
|
|
24
|
+
subscription (``CLAUDE_CODE_OAUTH_TOKEN``, a Codex ChatGPT login) is
|
|
25
|
+
unavailable for CI on every repository, personal or organization-owned.
|
|
26
|
+
The repository owner (:func:`repo_is_org`) is still reported but no longer
|
|
27
|
+
changes the rule.
|
|
27
28
|
|
|
28
29
|
Nothing here decides anything about the run itself: no verdicts, no stall
|
|
29
30
|
detection, no rendering. The server does that.
|
|
@@ -44,7 +45,7 @@ from pathlib import Path
|
|
|
44
45
|
from typing import Mapping, MutableMapping
|
|
45
46
|
|
|
46
47
|
|
|
47
|
-
from brindle import providers
|
|
48
|
+
from brindle import model_access, providers
|
|
48
49
|
from brindle.secrets import is_job_secret
|
|
49
50
|
|
|
50
51
|
log = logging.getLogger(__name__)
|
|
@@ -57,7 +58,8 @@ RATE_LIMIT = "rate_limit"
|
|
|
57
58
|
AUTH = "auth"
|
|
58
59
|
|
|
59
60
|
CLAUDE_API_KEYS = ("ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN")
|
|
60
|
-
CLAUDE_CLOUD = ("CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX", "CLAUDE_CODE_USE_FOUNDRY"
|
|
61
|
+
CLAUDE_CLOUD = ("CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX", "CLAUDE_CODE_USE_FOUNDRY",
|
|
62
|
+
"CLAUDE_CODE_USE_MANTLE")
|
|
61
63
|
CLAUDE_SUBSCRIPTION = ("CLAUDE_CODE_OAUTH_TOKEN",)
|
|
62
64
|
CODEX_API_KEYS = ("OPENAI_API_KEY", "CODEX_API_KEY")
|
|
63
65
|
CODEX_LOGIN = "codex login (auth.json)" # the name shown for a ChatGPT sign-in
|
|
@@ -181,6 +183,11 @@ class Adapter:
|
|
|
181
183
|
person answers), or None. Default: never."""
|
|
182
184
|
return None
|
|
183
185
|
|
|
186
|
+
def notices(self, db, root) -> list[str]:
|
|
187
|
+
"""Warnings the CLI printed that a run's notes should carry (Claude
|
|
188
|
+
Code's alias fallback to an older model). Default: none."""
|
|
189
|
+
return []
|
|
190
|
+
|
|
184
191
|
def review(self, instructions: str, cwd: str, env: Mapping[str, str], *,
|
|
185
192
|
timeout: float = REVIEW_TIMEOUT, profile: str | None = None) -> Review:
|
|
186
193
|
raise AdapterError(f"{self.name} can't review")
|
|
@@ -340,12 +347,30 @@ class ClaudeAdapter(Adapter):
|
|
|
340
347
|
since = now
|
|
341
348
|
self._api_errors[agent.id] = (error, since)
|
|
342
349
|
line = api_error_line(error)
|
|
350
|
+
cause = model_access.classify(error)
|
|
351
|
+
if cause is not None:
|
|
352
|
+
# The account can't call the model: no retry will help.
|
|
353
|
+
# under the repo's fingerprint, which session_event reads the notes with
|
|
354
|
+
ws = db.get_workspace(agent.workspace_id)
|
|
355
|
+
model_access.refuse(agent.profile, cause, repo_root=ws.repo_root if ws else None)
|
|
356
|
+
return f"{who} can't call its model: {cause.line()} ({line})"
|
|
343
357
|
if providers.ClaudeCode.FATAL_API_ERROR.match(error):
|
|
344
358
|
return f"{who} stopped on an error it won't retry: {line}"
|
|
345
359
|
if now - since >= TRANSIENT_API_ERROR_S:
|
|
346
360
|
return f"{who} has been stopped on an API error for {int(now - since)}s: {line}"
|
|
347
361
|
return None
|
|
348
362
|
|
|
363
|
+
def notices(self, db, root) -> list[str]:
|
|
364
|
+
from brindle import agents
|
|
365
|
+
|
|
366
|
+
found = []
|
|
367
|
+
for agent in [root, *(a for a in (agents.tree(db, root.id) if root else [])
|
|
368
|
+
if a.id != root.id and a.provider == self.name)]:
|
|
369
|
+
warning = model_access.alias_fallback(self._screen(agent) or "")
|
|
370
|
+
if warning and warning not in found:
|
|
371
|
+
found.append(warning)
|
|
372
|
+
return found
|
|
373
|
+
|
|
349
374
|
@staticmethod
|
|
350
375
|
def _screen(root) -> str | None:
|
|
351
376
|
from brindle import agents, tmux
|
|
@@ -610,10 +635,10 @@ def usable(adapter: Adapter, env: Mapping[str, str], org: bool | None) -> tuple[
|
|
|
610
635
|
ok, why = adapter.available(env)
|
|
611
636
|
if not ok:
|
|
612
637
|
return False, why
|
|
613
|
-
if adapter.credential(env).kind == SUBSCRIPTION
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
638
|
+
if adapter.credential(env).kind == SUBSCRIPTION:
|
|
639
|
+
return False, ("its only credential is a personal subscription, which CI may not use "
|
|
640
|
+
"(Anthropic's terms bar third parties from routing subscription credentials): "
|
|
641
|
+
"use an API key or a cloud sign-in")
|
|
617
642
|
return True, ""
|
|
618
643
|
|
|
619
644
|
|
|
@@ -642,6 +667,53 @@ def doctor(adapters: Mapping[str, Adapter], env: Mapping[str, str], org: bool |
|
|
|
642
667
|
return rows
|
|
643
668
|
|
|
644
669
|
|
|
670
|
+
def pinned_models(cwd: str) -> list[tuple[str, str]]:
|
|
671
|
+
"""(profile, model) for each distinct model a Claude profile in the routing
|
|
672
|
+
tiers pins, in routing order."""
|
|
673
|
+
from brindle.config import DEFAULT_ROUTING, load_repo_config
|
|
674
|
+
from brindle.profiles import load_profile
|
|
675
|
+
|
|
676
|
+
try:
|
|
677
|
+
routing = load_repo_config(cwd).routing
|
|
678
|
+
except ValueError:
|
|
679
|
+
routing = DEFAULT_ROUTING
|
|
680
|
+
seen: dict[str, str] = {}
|
|
681
|
+
for tier in routing.values():
|
|
682
|
+
for name in tier:
|
|
683
|
+
try:
|
|
684
|
+
p = load_profile(name, cwd)
|
|
685
|
+
except KeyError:
|
|
686
|
+
continue
|
|
687
|
+
if p.provider == "claude" and p.model and p.model not in seen:
|
|
688
|
+
seen[p.model] = name
|
|
689
|
+
return [(profile, model) for model, profile in seen.items()]
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def check_models(adapters: Mapping[str, Adapter], env: Mapping[str, str], cwd: str,
|
|
693
|
+
org: bool | None, *, run=subprocess.run) -> list[str]:
|
|
694
|
+
"""One tiny call per pinned model through the claude CLI: a line each,
|
|
695
|
+
with the cause and fix of a model-access refusal. A refused profile is
|
|
696
|
+
remembered, so routing moves its tasks to the next profile of the tier."""
|
|
697
|
+
claude = adapters.get("claude")
|
|
698
|
+
if claude is None or not usable(claude, env, org)[0]:
|
|
699
|
+
return ["models: skipped (claude isn't usable here)"]
|
|
700
|
+
pinned = pinned_models(cwd)
|
|
701
|
+
if not pinned:
|
|
702
|
+
return ["models: none pinned"]
|
|
703
|
+
binary = claude.cli_path(env) or "claude"
|
|
704
|
+
lines = []
|
|
705
|
+
for profile, model in pinned:
|
|
706
|
+
ok, cause, text = model_access.probe(binary, model, cwd, claude_env(env), run=run)
|
|
707
|
+
if ok:
|
|
708
|
+
lines.append(f"model {model} ({profile}): ok")
|
|
709
|
+
elif cause is not None:
|
|
710
|
+
model_access.refuse(profile, cause, repo_root=cwd)
|
|
711
|
+
lines.append(f"model {model} ({profile}): refused: {cause.line()}")
|
|
712
|
+
else:
|
|
713
|
+
lines.append(f"model {model} ({profile}): failed: {api_error_line(text)}")
|
|
714
|
+
return lines
|
|
715
|
+
|
|
716
|
+
|
|
645
717
|
def format_doctor(rows: list[dict], repo: str | None, org: bool | None) -> str:
|
|
646
718
|
owner = {True: "an organization", False: "a personal account"}.get(org, "unknown; treated as an organization")
|
|
647
719
|
lines = [f"repository: {repo or '(unknown)'}, owner is {owner}"]
|