brindle 0.3.0__tar.gz → 0.3.1__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.3.0 → brindle-0.3.1}/PKG-INFO +74 -3
- {brindle-0.3.0 → brindle-0.3.1}/README.md +73 -2
- {brindle-0.3.0 → brindle-0.3.1}/pyproject.toml +1 -1
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/agents.py +47 -5
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/cli.py +20 -3
- brindle-0.3.1/src/brindle/company_identity.py +531 -0
- brindle-0.3.1/src/brindle/company_login.py +416 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/cull.py +12 -4
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/doctor.py +59 -2
- brindle-0.3.1/src/brindle/identity_lock.py +71 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/keys_cmds.py +18 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/legacy.py +12 -1
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/mcp_server.py +16 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pane_auth.py +34 -3
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/account.py +10 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/team_policy.py +243 -2
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/providers.py +116 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/rewind.py +2 -1
- brindle-0.3.1/src/brindle/self_serve.py +344 -0
- brindle-0.3.1/src/brindle/signin_pause.py +384 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/tasks.py +8 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/view.py +12 -1
- {brindle-0.3.0 → brindle-0.3.1}/.gitignore +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/LICENSE +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/SCHEDULE-A +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/__init__.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/__main__.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/account.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/airgap.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/antigravity.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/autopilot.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/budget.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_agents/__init__.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_agents/backend-auditor.md +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_agents/developer-codex.md +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_agents/developer-heavy.md +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_agents/developer-light.md +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_agents/developer-local.md +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_agents/developer.md +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_agents/qa.md +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_agents/reviewer-codex.md +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_agents/reviewer-local.md +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_agents/reviewer.md +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_agents/subagent.md +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_agents/supervisor.md +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_rules/__init__.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_rules/security/backend.md +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_rules/style/minimal-diff.md +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/builtin_rules/tests/only.md +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/ci_adapters.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/ci_client.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/ci_federation.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/ci_hosts.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/codemap.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/codex_hook.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/config.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/conflicts.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/cost.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/cost_estimate.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/db.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/demo.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/detect.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/escapes.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/events.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/gates.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/git.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/guardrails.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/history.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/inbox.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/keystore.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/learned_rules.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/learning.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/model_access.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/native/__init__.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/native/client.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/native/loop.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/native/permissions.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/native/runner.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/native/serve.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/native/tools.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/permissions.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pipeline.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/plugins.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/policy.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pool.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pricing.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/__init__.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/_files.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/audit_chain.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/audit_export.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/auth.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/brief_learning.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/cost_centers.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/credentials.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/features.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/keys.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/learning.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/license.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/loopback.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/managed_models.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/org_profiles.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/orgkey.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/rollout.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/settings_sync.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/status.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/pro/team_events.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/procs.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/profiles.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/quota.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/repo_cmds.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/repos.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/rule_checks.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/savings.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/scratch.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/secrets.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/services.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/sessions.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/sidebar_follow.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/status_cache.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/tmux.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/usage.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/watch.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/watch_costs.py +0 -0
- {brindle-0.3.0 → brindle-0.3.1}/src/brindle/workspaces.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: brindle
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
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.
|
|
92
|
+
session shows it too (`brindle 0.3.1`). 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
|
|
@@ -363,6 +363,7 @@ your own status line prints, so what you see doesn't change.
|
|
|
363
363
|
| `brindle learning` | whether brindle Pro's hosted learning is on for this repo, and if not, why (nothing is learned on your machine) |
|
|
364
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 |
|
|
365
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) |
|
|
366
|
+
| `brindle login` | the same as `brindle account login`; when your org's policy has an `agent_setup` block it also signs you in to the Claude and Codex route the company chose (AWS SSO profile, `gcloud`, `az`, `claude auth login`, `codex login`), stores the Claude Code environment for brindle's panes in `~/.brindle/config.json`, and checks the pinned models. `brindle doctor --fix` does the same |
|
|
366
367
|
| `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) |
|
|
367
368
|
| `brindle watch [--all] [--once]` | the dashboard on its own (the same view as the sidebar): enter attaches, `p` peeks, `x` closes |
|
|
368
369
|
| `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 |
|
|
@@ -762,6 +763,7 @@ workflow.
|
|
|
762
763
|
|
|
763
764
|
```sh
|
|
764
765
|
brindle account # paid features: what you have, how to use them, how to get the rest
|
|
766
|
+
brindle login # the same as `brindle account login`; also applies your org's agent_setup (company agent sign-in)
|
|
765
767
|
brindle account login # opens your browser to sign in (--device: enter a code instead, e.g. over SSH); brindle checks the plan offline from then on
|
|
766
768
|
brindle account status # your plan, features, hosted learning on or off, when the entitlement expires
|
|
767
769
|
brindle account savings # what hosted learning's picks gained in this repo, this month and last: estimates, from this machine's records only
|
|
@@ -778,6 +780,51 @@ brindle account org member policy-role <member> <role|none> # give a member a
|
|
|
778
780
|
brindle account org company [link <org_id> | unlink] # link orgs you own into one company; learning is pooled only within it
|
|
779
781
|
```
|
|
780
782
|
|
|
783
|
+
**Sign-in loss pauses agents.** While agents run, brindle checks every 30 s that
|
|
784
|
+
Claude and Codex are still signed in (`claude auth status` or the cloud route's
|
|
785
|
+
identity, re-checked only when its credentials change; `codex login status`;
|
|
786
|
+
Antigravity can't be checked). If one is signed out, its running agents pause
|
|
787
|
+
with their worktrees, commits and conversations kept, and the supervisor and
|
|
788
|
+
your session are told; a timeout or missing CLI never pauses, it warns once.
|
|
789
|
+
A wrong company identity pauses them only when the org policy enforces it
|
|
790
|
+
(Enterprise); otherwise it warns once. While it enforces, Claude agents run
|
|
791
|
+
on exactly the route brindle checked: a profile's env lines can't change the
|
|
792
|
+
backend, account, key or proxy, and other cloud and Claude credential variables
|
|
793
|
+
(`ANTHROPIC_*`, `AWS_*`, `GOOGLE_*`, `CLOUDSDK_*`, `AZURE_*`, ...) in your
|
|
794
|
+
environment or `brindle keys` are left out of their panes, so those agents
|
|
795
|
+
can't use your own AWS, Google Cloud or Azure access in their shell either.
|
|
796
|
+
Claude Code's own settings files can still set variables inside it; to rule
|
|
797
|
+
that out too, use Claude Code's managed settings. After `brindle login`,
|
|
798
|
+
`brindle doctor --fix` or the CLI's own login, the agents brindle paused resume
|
|
799
|
+
into their conversations, but only as the account they started under: if
|
|
800
|
+
someone else is signed in (a personal login, say) they stay paused until the
|
|
801
|
+
original account is back. Your own chat session is never paused, only the
|
|
802
|
+
workers. A cloud sign-in is re-checked only when its credential files or
|
|
803
|
+
variables change, so a token that expires silently isn't noticed until then.
|
|
804
|
+
`brindle doctor` lists agents paused for sign-in.
|
|
805
|
+
|
|
806
|
+
**Sessions stay with their identity.** Each Claude agent records the identity it
|
|
807
|
+
launched under (Claude org, AWS account and role, GCP project, Azure
|
|
808
|
+
subscription; ids only, never tokens). While a different identity is signed in,
|
|
809
|
+
that agent is locked: brindle refuses to resume it (`brindle continue`), rewind
|
|
810
|
+
it, message or hand off to it, start a new agent in its worktree (including
|
|
811
|
+
naming its branch for a new worker), or `peek` at it, with "This session
|
|
812
|
+
belongs to <org/account>; sign in as it to continue (`brindle login`)".
|
|
813
|
+
`brindle ls`, `watch`, the sidebar, `list_agents` and `agent_turns` show it as
|
|
814
|
+
`locked: <org/account>` without its conversation (usage, last activity, turn
|
|
815
|
+
summaries), and `depends_on` and `merge_workspace` won't pull its branch into a
|
|
816
|
+
session under another identity. Agents with no recorded identity (started before
|
|
817
|
+
this, or without a company setup) are unaffected, and so is everything while the
|
|
818
|
+
current identity can't be told (signed out, CLI missing).
|
|
819
|
+
|
|
820
|
+
This is a guard inside brindle, not a sandbox: on one OS user account the
|
|
821
|
+
worktrees, transcripts and credential files stay readable outside brindle (a
|
|
822
|
+
shell, another tool). Real separation needs a separate OS user or your
|
|
823
|
+
company's device management. Codex and Antigravity can't be told apart by
|
|
824
|
+
account (brindle only sees "signed in"), so their sessions are paused on
|
|
825
|
+
sign-out but can't be locked to one account; isolation applies to Claude routes
|
|
826
|
+
(subscription, Bedrock, Vertex, Foundry).
|
|
827
|
+
|
|
781
828
|
### Several repos in one session (brindle Pro)
|
|
782
829
|
|
|
783
830
|
A session can work across several repos. Attach another local repo you own
|
|
@@ -1258,7 +1305,7 @@ it's signed in without a plan that includes it, is reported to its supervisor.
|
|
|
1258
1305
|
| Provider | Key login (environment variables) |
|
|
1259
1306
|
|---|---|
|
|
1260
1307
|
| `claude` | yes: `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, `CLAUDE_CODE_OAUTH_TOKEN`, or Bedrock/Vertex/Foundry (`CLAUDE_CODE_USE_BEDROCK`, `CLAUDE_CODE_USE_VERTEX`, `CLAUDE_CODE_USE_FOUNDRY`) |
|
|
1261
|
-
| `codex` | yes: `OPENAI_API_KEY`, `CODEX_API_KEY` |
|
|
1308
|
+
| `codex` | yes: `OPENAI_API_KEY`, `CODEX_API_KEY`. With Codex signed in with ChatGPT (checked on Codex CLI 0.162.0): interactive `codex`, which brindle runs in agent panes, ignores both keys and uses the ChatGPT plan, and only `codex login --with-api-key` switches it to a key; headless `codex exec` (Brindle-CI) bills `CODEX_API_KEY` over the login and ignores `OPENAI_API_KEY`. `brindle doctor` and `brindle keys set` say so, and `auth: api_key` on an interactive Codex profile is refused under a ChatGPT login (run `codex login --with-api-key`, or use `auth: subscription`) |
|
|
1262
1309
|
| `antigravity` | `GEMINI_API_KEY`, only with `"modelProvider": "gemini"` in `agy`'s `settings.json` (see [Google Antigravity](#google-antigravity)); otherwise `agy`'s own browser sign-in |
|
|
1263
1310
|
|
|
1264
1311
|
Workers that run unattended should use key login where the CLI supports it.
|
|
@@ -1666,6 +1713,30 @@ marks the agent idle and delivers any queued message. Codex has no Stop hook, so
|
|
|
1666
1713
|
an autopilot supervisor on Codex that turn end is also where brindle tells it to keep
|
|
1667
1714
|
going, with the same limit on reminders as Claude Code.
|
|
1668
1715
|
|
|
1716
|
+
### Using your company's access
|
|
1717
|
+
|
|
1718
|
+
Two ways to run brindle's agents on your company's Claude (Bedrock, Vertex,
|
|
1719
|
+
Foundry, a Team or Enterprise plan) and Codex, without handling keys:
|
|
1720
|
+
|
|
1721
|
+
- **Your org sets it up (brindle Team).** When your org's policy has an
|
|
1722
|
+
`agent_setup` block, `brindle login` (and `brindle doctor --fix`) signs you
|
|
1723
|
+
in to the route the company chose and `brindle doctor` shows which identity
|
|
1724
|
+
each agent uses. It always wins over anything below.
|
|
1725
|
+
- **You set it up yourself (any plan).** When your org has no `agent_setup`,
|
|
1726
|
+
`brindle login` looks, read-only, at what this machine is already signed in
|
|
1727
|
+
to: Claude Code's managed settings (if your IT configures Claude Code,
|
|
1728
|
+
brindle just uses it), `claude auth status`, AWS SSO profiles in
|
|
1729
|
+
`~/.aws/config`, the active `gcloud` project, the active `az` subscription
|
|
1730
|
+
and `codex login status`. It never reads tokens or credential files. It
|
|
1731
|
+
proposes what it found ("Use Bedrock (AWS 1234…, role Dev, us-east-1)?");
|
|
1732
|
+
the default is no, and with no terminal it is always no. A yes is saved as
|
|
1733
|
+
`"agent_setup"` in `~/.brindle/config.json`, in the same format as the org
|
|
1734
|
+
policy's (`enforce` is never applied to it), and `brindle doctor` labels it
|
|
1735
|
+
as self-serve and says who is billed.
|
|
1736
|
+
|
|
1737
|
+
You must be authorized to use any company credentials you connect: brindle only
|
|
1738
|
+
signs in with the access you already have, it doesn't grant any.
|
|
1739
|
+
|
|
1669
1740
|
## Google Antigravity
|
|
1670
1741
|
|
|
1671
1742
|
brindle runs Google Antigravity's terminal agent, `agy`, as well as Claude Code and
|
|
@@ -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.
|
|
62
|
+
session shows it too (`brindle 0.3.1`). 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
|
|
@@ -333,6 +333,7 @@ your own status line prints, so what you see doesn't change.
|
|
|
333
333
|
| `brindle learning` | whether brindle Pro's hosted learning is on for this repo, and if not, why (nothing is learned on your machine) |
|
|
334
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 |
|
|
335
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) |
|
|
336
|
+
| `brindle login` | the same as `brindle account login`; when your org's policy has an `agent_setup` block it also signs you in to the Claude and Codex route the company chose (AWS SSO profile, `gcloud`, `az`, `claude auth login`, `codex login`), stores the Claude Code environment for brindle's panes in `~/.brindle/config.json`, and checks the pinned models. `brindle doctor --fix` does the same |
|
|
336
337
|
| `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) |
|
|
337
338
|
| `brindle watch [--all] [--once]` | the dashboard on its own (the same view as the sidebar): enter attaches, `p` peeks, `x` closes |
|
|
338
339
|
| `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 |
|
|
@@ -732,6 +733,7 @@ workflow.
|
|
|
732
733
|
|
|
733
734
|
```sh
|
|
734
735
|
brindle account # paid features: what you have, how to use them, how to get the rest
|
|
736
|
+
brindle login # the same as `brindle account login`; also applies your org's agent_setup (company agent sign-in)
|
|
735
737
|
brindle account login # opens your browser to sign in (--device: enter a code instead, e.g. over SSH); brindle checks the plan offline from then on
|
|
736
738
|
brindle account status # your plan, features, hosted learning on or off, when the entitlement expires
|
|
737
739
|
brindle account savings # what hosted learning's picks gained in this repo, this month and last: estimates, from this machine's records only
|
|
@@ -748,6 +750,51 @@ brindle account org member policy-role <member> <role|none> # give a member a
|
|
|
748
750
|
brindle account org company [link <org_id> | unlink] # link orgs you own into one company; learning is pooled only within it
|
|
749
751
|
```
|
|
750
752
|
|
|
753
|
+
**Sign-in loss pauses agents.** While agents run, brindle checks every 30 s that
|
|
754
|
+
Claude and Codex are still signed in (`claude auth status` or the cloud route's
|
|
755
|
+
identity, re-checked only when its credentials change; `codex login status`;
|
|
756
|
+
Antigravity can't be checked). If one is signed out, its running agents pause
|
|
757
|
+
with their worktrees, commits and conversations kept, and the supervisor and
|
|
758
|
+
your session are told; a timeout or missing CLI never pauses, it warns once.
|
|
759
|
+
A wrong company identity pauses them only when the org policy enforces it
|
|
760
|
+
(Enterprise); otherwise it warns once. While it enforces, Claude agents run
|
|
761
|
+
on exactly the route brindle checked: a profile's env lines can't change the
|
|
762
|
+
backend, account, key or proxy, and other cloud and Claude credential variables
|
|
763
|
+
(`ANTHROPIC_*`, `AWS_*`, `GOOGLE_*`, `CLOUDSDK_*`, `AZURE_*`, ...) in your
|
|
764
|
+
environment or `brindle keys` are left out of their panes, so those agents
|
|
765
|
+
can't use your own AWS, Google Cloud or Azure access in their shell either.
|
|
766
|
+
Claude Code's own settings files can still set variables inside it; to rule
|
|
767
|
+
that out too, use Claude Code's managed settings. After `brindle login`,
|
|
768
|
+
`brindle doctor --fix` or the CLI's own login, the agents brindle paused resume
|
|
769
|
+
into their conversations, but only as the account they started under: if
|
|
770
|
+
someone else is signed in (a personal login, say) they stay paused until the
|
|
771
|
+
original account is back. Your own chat session is never paused, only the
|
|
772
|
+
workers. A cloud sign-in is re-checked only when its credential files or
|
|
773
|
+
variables change, so a token that expires silently isn't noticed until then.
|
|
774
|
+
`brindle doctor` lists agents paused for sign-in.
|
|
775
|
+
|
|
776
|
+
**Sessions stay with their identity.** Each Claude agent records the identity it
|
|
777
|
+
launched under (Claude org, AWS account and role, GCP project, Azure
|
|
778
|
+
subscription; ids only, never tokens). While a different identity is signed in,
|
|
779
|
+
that agent is locked: brindle refuses to resume it (`brindle continue`), rewind
|
|
780
|
+
it, message or hand off to it, start a new agent in its worktree (including
|
|
781
|
+
naming its branch for a new worker), or `peek` at it, with "This session
|
|
782
|
+
belongs to <org/account>; sign in as it to continue (`brindle login`)".
|
|
783
|
+
`brindle ls`, `watch`, the sidebar, `list_agents` and `agent_turns` show it as
|
|
784
|
+
`locked: <org/account>` without its conversation (usage, last activity, turn
|
|
785
|
+
summaries), and `depends_on` and `merge_workspace` won't pull its branch into a
|
|
786
|
+
session under another identity. Agents with no recorded identity (started before
|
|
787
|
+
this, or without a company setup) are unaffected, and so is everything while the
|
|
788
|
+
current identity can't be told (signed out, CLI missing).
|
|
789
|
+
|
|
790
|
+
This is a guard inside brindle, not a sandbox: on one OS user account the
|
|
791
|
+
worktrees, transcripts and credential files stay readable outside brindle (a
|
|
792
|
+
shell, another tool). Real separation needs a separate OS user or your
|
|
793
|
+
company's device management. Codex and Antigravity can't be told apart by
|
|
794
|
+
account (brindle only sees "signed in"), so their sessions are paused on
|
|
795
|
+
sign-out but can't be locked to one account; isolation applies to Claude routes
|
|
796
|
+
(subscription, Bedrock, Vertex, Foundry).
|
|
797
|
+
|
|
751
798
|
### Several repos in one session (brindle Pro)
|
|
752
799
|
|
|
753
800
|
A session can work across several repos. Attach another local repo you own
|
|
@@ -1228,7 +1275,7 @@ it's signed in without a plan that includes it, is reported to its supervisor.
|
|
|
1228
1275
|
| Provider | Key login (environment variables) |
|
|
1229
1276
|
|---|---|
|
|
1230
1277
|
| `claude` | yes: `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, `CLAUDE_CODE_OAUTH_TOKEN`, or Bedrock/Vertex/Foundry (`CLAUDE_CODE_USE_BEDROCK`, `CLAUDE_CODE_USE_VERTEX`, `CLAUDE_CODE_USE_FOUNDRY`) |
|
|
1231
|
-
| `codex` | yes: `OPENAI_API_KEY`, `CODEX_API_KEY` |
|
|
1278
|
+
| `codex` | yes: `OPENAI_API_KEY`, `CODEX_API_KEY`. With Codex signed in with ChatGPT (checked on Codex CLI 0.162.0): interactive `codex`, which brindle runs in agent panes, ignores both keys and uses the ChatGPT plan, and only `codex login --with-api-key` switches it to a key; headless `codex exec` (Brindle-CI) bills `CODEX_API_KEY` over the login and ignores `OPENAI_API_KEY`. `brindle doctor` and `brindle keys set` say so, and `auth: api_key` on an interactive Codex profile is refused under a ChatGPT login (run `codex login --with-api-key`, or use `auth: subscription`) |
|
|
1232
1279
|
| `antigravity` | `GEMINI_API_KEY`, only with `"modelProvider": "gemini"` in `agy`'s `settings.json` (see [Google Antigravity](#google-antigravity)); otherwise `agy`'s own browser sign-in |
|
|
1233
1280
|
|
|
1234
1281
|
Workers that run unattended should use key login where the CLI supports it.
|
|
@@ -1636,6 +1683,30 @@ marks the agent idle and delivers any queued message. Codex has no Stop hook, so
|
|
|
1636
1683
|
an autopilot supervisor on Codex that turn end is also where brindle tells it to keep
|
|
1637
1684
|
going, with the same limit on reminders as Claude Code.
|
|
1638
1685
|
|
|
1686
|
+
### Using your company's access
|
|
1687
|
+
|
|
1688
|
+
Two ways to run brindle's agents on your company's Claude (Bedrock, Vertex,
|
|
1689
|
+
Foundry, a Team or Enterprise plan) and Codex, without handling keys:
|
|
1690
|
+
|
|
1691
|
+
- **Your org sets it up (brindle Team).** When your org's policy has an
|
|
1692
|
+
`agent_setup` block, `brindle login` (and `brindle doctor --fix`) signs you
|
|
1693
|
+
in to the route the company chose and `brindle doctor` shows which identity
|
|
1694
|
+
each agent uses. It always wins over anything below.
|
|
1695
|
+
- **You set it up yourself (any plan).** When your org has no `agent_setup`,
|
|
1696
|
+
`brindle login` looks, read-only, at what this machine is already signed in
|
|
1697
|
+
to: Claude Code's managed settings (if your IT configures Claude Code,
|
|
1698
|
+
brindle just uses it), `claude auth status`, AWS SSO profiles in
|
|
1699
|
+
`~/.aws/config`, the active `gcloud` project, the active `az` subscription
|
|
1700
|
+
and `codex login status`. It never reads tokens or credential files. It
|
|
1701
|
+
proposes what it found ("Use Bedrock (AWS 1234…, role Dev, us-east-1)?");
|
|
1702
|
+
the default is no, and with no terminal it is always no. A yes is saved as
|
|
1703
|
+
`"agent_setup"` in `~/.brindle/config.json`, in the same format as the org
|
|
1704
|
+
policy's (`enforce` is never applied to it), and `brindle doctor` labels it
|
|
1705
|
+
as self-serve and says who is billed.
|
|
1706
|
+
|
|
1707
|
+
You must be authorized to use any company credentials you connect: brindle only
|
|
1708
|
+
signs in with the access you already have, it doesn't grant any.
|
|
1709
|
+
|
|
1639
1710
|
## Google Antigravity
|
|
1640
1711
|
|
|
1641
1712
|
brindle runs Google Antigravity's terminal agent, `agy`, as well as Claude Code and
|
|
@@ -20,7 +20,7 @@ import sys
|
|
|
20
20
|
import time
|
|
21
21
|
import uuid
|
|
22
22
|
|
|
23
|
-
from brindle import git, keystore, pane_auth, providers, secrets, tmux, workspaces
|
|
23
|
+
from brindle import company_identity, company_login, 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
|
|
@@ -183,7 +183,19 @@ def agent_env(ws: Workspace, agent_id: str, agent: Agent | None = None,
|
|
|
183
183
|
# Claude Code's own managed settings (apiKeyHelper, a managed env) pick
|
|
184
184
|
# the credential and backend: brindle adds no provider_config env there.
|
|
185
185
|
org = agent.provider == "claude" and providers.claude_org_managed()
|
|
186
|
-
|
|
186
|
+
# The company's Claude Code env (`brindle login`): the lowest layer, so a
|
|
187
|
+
# profile's env lines and the org's managed provider both beat it; none
|
|
188
|
+
# at all under Claude Code's managed settings (stored_env).
|
|
189
|
+
company = {} if agent.provider != "claude" else company_login.stored_env()
|
|
190
|
+
managed_env = {} if org else managed_models.agent_env(m, agent.provider)
|
|
191
|
+
enforced = None if agent.provider != "claude" or org else company_identity.enforced_route_env()
|
|
192
|
+
if enforced is None:
|
|
193
|
+
env = {**company, **profile_env, **managed_env, **env}
|
|
194
|
+
else:
|
|
195
|
+
# An enforced company identity (Enterprise): the pane runs on the route
|
|
196
|
+
# launch_problem checked, whatever a profile's env lines say.
|
|
197
|
+
profile_env = {k: v for k, v in profile_env.items() if company_identity.profile_may_set(k)}
|
|
198
|
+
env = {**profile_env, **managed_env, **enforced, **env}
|
|
187
199
|
if agent is not None and preload_tools(agent, ws):
|
|
188
200
|
# Claude Code defers MCP tools and loads them on demand, which costs a
|
|
189
201
|
# worker an extra round trip at the moment it's told to report (and
|
|
@@ -262,7 +274,9 @@ def spawn(
|
|
|
262
274
|
finish line run it as a Claude Code ``/goal``. With ``autopilot``, the
|
|
263
275
|
agent is a session root that drives toward a goal (see brindle.autopilot)."""
|
|
264
276
|
from brindle import autopilot as pilot
|
|
277
|
+
from brindle import identity_lock
|
|
265
278
|
|
|
279
|
+
identity_lock.ensure_workspace(db, ws) # no new agent in another identity's worktree
|
|
266
280
|
profile = load_profile(profile_name, ws.repo_root)
|
|
267
281
|
provider = get_provider(provider_name or profile.provider)
|
|
268
282
|
agent_id = new_id()
|
|
@@ -440,6 +454,12 @@ def _open_window(db: DB, agent: Agent, ws: Workspace, name: str, argv: list[str]
|
|
|
440
454
|
org_env = managed_models.org_key_env(m, agent.provider)
|
|
441
455
|
except managed_models.ManagedUnavailable as e:
|
|
442
456
|
raise AgentError(f"managed models: {e}") from None
|
|
457
|
+
# An enforced company identity: also every account picker the tmux server
|
|
458
|
+
# would hand the pane or `brindle keys` holds for it (pane_auth covers this process's).
|
|
459
|
+
route = None if agent.provider != "claude" or org_managed else company_identity.enforced_route_env()
|
|
460
|
+
if route is not None:
|
|
461
|
+
extra = company_identity.enforced_deny(route, tmux.inherited_names(ws.tmux_session), credentials)
|
|
462
|
+
deny = (*deny, *(n for n in extra if n not in deny))
|
|
443
463
|
# Claude Code's managed settings supply the credential: no stored Claude key.
|
|
444
464
|
no_stored = set(deny) | (set(providers._ENV_AUTH["claude"]) if org_managed else set())
|
|
445
465
|
env = {**keystore.pane_keys(credentials - no_stored, {**env, **org_env}), **env, **org_env}
|
|
@@ -612,6 +632,14 @@ def _launch(db: DB, agent: Agent, ws: Workspace, *, prompt: str | None,
|
|
|
612
632
|
db.set_status(agent.id, status)
|
|
613
633
|
agent.status = status
|
|
614
634
|
return
|
|
635
|
+
# An org that enforces its company sign-in (Enterprise) refuses a
|
|
636
|
+
# different identity; one that can't be told never blocks.
|
|
637
|
+
why = company_identity.launch_problem(provider.name)
|
|
638
|
+
if why:
|
|
639
|
+
raise AgentError(why)
|
|
640
|
+
from brindle import signin_pause
|
|
641
|
+
|
|
642
|
+
signin_pause.record_launch(agent) # who it runs as, so a resume never crosses accounts
|
|
615
643
|
# What the agent will run with: the managed provider's variables, and
|
|
616
644
|
# no personal keys (blank, so the launcher's own copy doesn't count).
|
|
617
645
|
from brindle.pro import managed_models
|
|
@@ -906,10 +934,14 @@ def resume(db: DB, root_id: str, *, watch_pane: bool = True,
|
|
|
906
934
|
restart in their own workspaces. Claude Code picks up its previous
|
|
907
935
|
conversation (--resume); other CLIs restart on their original task.
|
|
908
936
|
With ``only``, just those agents (if paused) restart."""
|
|
937
|
+
from brindle import identity_lock
|
|
938
|
+
|
|
939
|
+
candidates = [a for a in tree(db, root_id)
|
|
940
|
+
if a.status == "paused" and (only is None or a.id in only)]
|
|
941
|
+
for a in candidates: # all or nothing: a session never half-resumes under another identity
|
|
942
|
+
identity_lock.ensure(a)
|
|
909
943
|
resumed = []
|
|
910
|
-
for a in
|
|
911
|
-
if a.status != "paused" or (only is not None and a.id not in only):
|
|
912
|
-
continue
|
|
944
|
+
for a in candidates:
|
|
913
945
|
ws = db.get_workspace(a.workspace_id)
|
|
914
946
|
if ws is None or not os.path.isdir(ws.path):
|
|
915
947
|
continue
|
|
@@ -1138,7 +1170,10 @@ def send_message(db: DB, to_id: str, body: str, sender_id: str | None = None,
|
|
|
1138
1170
|
"""Deliver now if the agent is idle; otherwise queue until it is.
|
|
1139
1171
|
Returns ``"delivered"`` or ``"queued"``. ``person`` marks a message from
|
|
1140
1172
|
the person running brindle, which is always pushed."""
|
|
1173
|
+
from brindle import identity_lock
|
|
1174
|
+
|
|
1141
1175
|
agent = get(db, to_id)
|
|
1176
|
+
identity_lock.ensure(agent)
|
|
1142
1177
|
if not person and runs_process(agent) and is_alive(agent) and pulls_messages(db, agent):
|
|
1143
1178
|
# Keep the message unread and deliver only a notice, once for everything unread.
|
|
1144
1179
|
db.enqueue_held(agent.id, format_message(db, body, sender_id), sender_id)
|
|
@@ -1648,6 +1683,13 @@ def delegate(
|
|
|
1648
1683
|
raise AgentError(str(e)) from e
|
|
1649
1684
|
if plan_first is None:
|
|
1650
1685
|
plan_first = load_repo_config(caller_ws.repo_root).plan_first
|
|
1686
|
+
from brindle import identity_lock
|
|
1687
|
+
|
|
1688
|
+
identity_lock.ensure_workspace(db, caller_ws)
|
|
1689
|
+
if isolate and branch: # naming a branch must not reuse another identity's worktree
|
|
1690
|
+
for other in db.find_workspaces(caller_ws.repo_root):
|
|
1691
|
+
if other.branch == branch:
|
|
1692
|
+
identity_lock.ensure_workspace(db, other)
|
|
1651
1693
|
if isolate:
|
|
1652
1694
|
caller_ws = workspaces.refresh_branch(db, caller_ws)
|
|
1653
1695
|
# merge_into applies to the supervisor's workers; a worker's own
|
|
@@ -13,7 +13,7 @@ from typing import Optional
|
|
|
13
13
|
|
|
14
14
|
import typer
|
|
15
15
|
|
|
16
|
-
from brindle import agents, git, tmux, view, workspaces
|
|
16
|
+
from brindle import agents, git, identity_lock, tmux, view, workspaces
|
|
17
17
|
from brindle import history as history_mod
|
|
18
18
|
from brindle import keys_cmds, repo_cmds
|
|
19
19
|
from brindle.usage import format_tokens
|
|
@@ -792,7 +792,7 @@ def autopilot_cmd(
|
|
|
792
792
|
|
|
793
793
|
@app.command()
|
|
794
794
|
def doctor(
|
|
795
|
-
fix: bool = typer.Option(False, "--fix", help="Offer to let Claude Code use an exported ANTHROPIC_API_KEY without asking."),
|
|
795
|
+
fix: bool = typer.Option(False, "--fix", help="Offer to let Claude Code use an exported ANTHROPIC_API_KEY without asking, and agy a GEMINI_API_KEY (modelProvider \"gemini\")."),
|
|
796
796
|
) -> None:
|
|
797
797
|
"""Check that brindle has what it needs, and say what to do about anything missing.
|
|
798
798
|
|
|
@@ -1655,6 +1655,17 @@ def learning_seed() -> None:
|
|
|
1655
1655
|
"help_option_names": []}) # --help is the plugin's: it lists every subcommand
|
|
1656
1656
|
def account(ctx: typer.Context) -> None:
|
|
1657
1657
|
"""brindle Pro/Team: paid features, login, upgrade, billing, orgs. Bare `brindle account` shows what you have."""
|
|
1658
|
+
_run_account(list(ctx.args))
|
|
1659
|
+
|
|
1660
|
+
|
|
1661
|
+
@app.command("login", context_settings={"allow_extra_args": True, "ignore_unknown_options": True,
|
|
1662
|
+
"help_option_names": []})
|
|
1663
|
+
def login(ctx: typer.Context) -> None:
|
|
1664
|
+
"""Log in to brindle and set up your company's agents: the same as `brindle account login`."""
|
|
1665
|
+
_run_account(["login", *ctx.args])
|
|
1666
|
+
|
|
1667
|
+
|
|
1668
|
+
def _run_account(args: list[str]) -> None:
|
|
1658
1669
|
from brindle import account as account_mod
|
|
1659
1670
|
from brindle.config import load_repo_config
|
|
1660
1671
|
|
|
@@ -1668,7 +1679,7 @@ def account(ctx: typer.Context) -> None:
|
|
|
1668
1679
|
from brindle.config import RepoConfig
|
|
1669
1680
|
|
|
1670
1681
|
cfg = RepoConfig()
|
|
1671
|
-
raise typer.Exit(account_mod.run(cfg, repo_root,
|
|
1682
|
+
raise typer.Exit(account_mod.run(cfg, repo_root, args, echo=typer.echo))
|
|
1672
1683
|
|
|
1673
1684
|
|
|
1674
1685
|
# -- brindle audit (brindle Enterprise: the local tamper-evident audit chain) ---------------------
|
|
@@ -2141,6 +2152,7 @@ def agent_peek(agent_id: str, lines: int = typer.Option(40, "--lines", "-n")) ->
|
|
|
2141
2152
|
"""Print the last lines of an agent's terminal."""
|
|
2142
2153
|
db = DB()
|
|
2143
2154
|
a = _run(agents.get, db, agent_id)
|
|
2155
|
+
_run(identity_lock.ensure, a)
|
|
2144
2156
|
if not a.tmux_window:
|
|
2145
2157
|
typer.secho(f"{a.id} has no terminal (it runs as its supervisor's own subagent)"
|
|
2146
2158
|
if not agents.runs_process(a) else f"{a.id} has no terminal", fg="red", err=True)
|
|
@@ -2158,6 +2170,11 @@ def agent_turns(agent_id: str) -> None:
|
|
|
2158
2170
|
ws = db.get_workspace(a.workspace_id)
|
|
2159
2171
|
if ws is None:
|
|
2160
2172
|
_fail(f"{a.id}'s workspace is gone")
|
|
2173
|
+
from brindle import identity_lock
|
|
2174
|
+
|
|
2175
|
+
who = identity_lock.locked_by(a.id)
|
|
2176
|
+
if who:
|
|
2177
|
+
_fail(f"{a.id} is locked: {who}. {identity_lock.message(who)}.")
|
|
2161
2178
|
typer.echo(rewind.format_turns(ws.repo_root, a.id))
|
|
2162
2179
|
|
|
2163
2180
|
|