brindle 0.3.17__tar.gz → 0.3.18__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.17 → brindle-0.3.18}/PKG-INFO +9 -6
- {brindle-0.3.17 → brindle-0.3.18}/README.md +8 -5
- {brindle-0.3.17 → brindle-0.3.18}/pyproject.toml +1 -1
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/agents.py +58 -6
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/autopilot.py +82 -17
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/cli.py +29 -5
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/company_identity.py +9 -3
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/company_login.py +13 -1
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/cull.py +96 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/db.py +21 -5
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/git.py +40 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/launch_health.py +77 -4
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/mcp_server.py +14 -6
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/pipeline.py +2 -1
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/pro/account.py +6 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/pro/managed_models.py +6 -2
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/pro/team_policy.py +16 -2
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/prompt_answer.py +25 -1
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/providers.py +10 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/view.py +5 -2
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/watch.py +2 -1
- {brindle-0.3.17 → brindle-0.3.18}/.gitignore +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/LICENSE +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/SCHEDULE-A +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/__init__.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/__main__.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/account.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/airgap.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/antigravity.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/budget.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/builtin_agents/__init__.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/builtin_agents/backend-auditor.md +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/builtin_agents/developer-codex.md +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/builtin_agents/developer-fable.md +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/builtin_agents/developer-gemini.md +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/builtin_agents/developer-heavy.md +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/builtin_agents/developer-light.md +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/builtin_agents/developer-local.md +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/builtin_agents/developer.md +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/builtin_agents/qa.md +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/builtin_agents/reviewer-codex.md +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/builtin_agents/reviewer-gemini.md +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/builtin_agents/reviewer-local.md +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/builtin_agents/reviewer.md +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/builtin_agents/subagent.md +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/builtin_agents/supervisor.md +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/builtin_rules/__init__.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/builtin_rules/security/backend.md +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/builtin_rules/style/minimal-diff.md +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/builtin_rules/tests/only.md +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/ci_adapters.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/ci_client.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/ci_federation.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/ci_hosts.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/cloud_credential.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/cloud_login.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/codemap.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/codemap_docs.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/codex_hook.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/config.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/conflicts.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/cost.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/cost_estimate.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/demo.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/detect.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/doctor.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/escapes.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/events.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/gates.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/guardrails.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/history.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/identity_lock.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/inbox.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/keys_cmds.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/keystore.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/learned_rules.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/learning.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/legacy.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/login_pause.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/model_access.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/model_availability.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/native/__init__.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/native/client.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/native/loop.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/native/permissions.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/native/runner.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/native/serve.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/native/tools.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/pane_auth.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/permissions.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/plugins.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/policy.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/pool.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/pricing.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/pro/__init__.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/pro/_files.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/pro/audit_chain.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/pro/audit_export.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/pro/auth.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/pro/brief_learning.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/pro/cost_centers.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/pro/credentials.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/pro/features.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/pro/keys.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/pro/learning.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/pro/license.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/pro/loopback.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/pro/org_profiles.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/pro/orgkey.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/pro/rollout.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/pro/route_health.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/pro/settings_sync.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/pro/status.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/pro/team_events.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/procs.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/profiles.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/quota.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/repo_cmds.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/repos.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/rewind.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/rule_checks.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/savings.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/scratch.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/secrets.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/self_serve.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/services.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/sessions.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/sidebar_follow.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/signin_pause.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/status_cache.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/tasks.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/tmux.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/updates.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/upgrade.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/usage.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/src/brindle/watch_costs.py +0 -0
- {brindle-0.3.17 → brindle-0.3.18}/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.18
|
|
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
|
|
@@ -94,7 +94,7 @@ uv tool install --editable ~/Projects/brindle # or from a local checkout
|
|
|
94
94
|
brindle drives Claude Code, so you need that too (`npm install -g @anthropic-ai/claude-code`).
|
|
95
95
|
|
|
96
96
|
`brindle --version` prints the installed version; the tmux status bar of every brindle
|
|
97
|
-
session shows it too (`brindle 0.3.
|
|
97
|
+
session shows it too (`brindle 0.3.18`). A session started before an upgrade keeps
|
|
98
98
|
running the old code, and shows the old number, until you restart it.
|
|
99
99
|
|
|
100
100
|
## Quick start
|
|
@@ -415,7 +415,7 @@ knowing them helps when you tell the supervisor how to work.
|
|
|
415
415
|
| `complete_subagent` | supervisor | record the result of a `subagent`-profile task |
|
|
416
416
|
| `set_goal` / `get_progress` / `check_milestone` | supervisor | autopilot's goal, its progress, and running the checks |
|
|
417
417
|
| `record_audit_decision` | supervisor | record acceptance of an audit finding with rationale so subsequent audits respect it |
|
|
418
|
-
| `need_user` | supervisor |
|
|
418
|
+
| `need_user` | supervisor | ask you a question (a decision, a denied action, a stalled external job such as a CI run no runner picks up); only the steps that depend on it wait, the rest keeps going; `answered=true` once you've replied |
|
|
419
419
|
| `transfer_to_repo` | supervisor | move a scratch session's work into a repository |
|
|
420
420
|
| `handover` | supervisor | hand the session to a new supervisor on another branch or worktree, with a note |
|
|
421
421
|
|
|
@@ -641,6 +641,8 @@ wins. When every candidate is out, the repo's `default_agent` runs. The reply sa
|
|
|
641
641
|
what was picked and why, e.g. `weight medium -> developer (Codex at 93%, skipped developer-codex)`.
|
|
642
642
|
An `agent_profile` you pass, or a milestone's `profile`, always wins over weight.
|
|
643
643
|
|
|
644
|
+
The supervisor runs on your Claude Code model. If your organisation's Claude Code managed settings pin the model (e.g. to Sonnet), that applies to the brindle supervisor too. Supervising goes best on Opus (or Fable where available), so if your org allows it, switch in the supervisor's own session with `/model`. Worker profiles set their own model and aren't affected.
|
|
645
|
+
|
|
644
646
|
Provisioning and routing also check explicit models against supported read-only
|
|
645
647
|
catalogs, with the managed model, stored keys and the worker's filtered pane
|
|
646
648
|
environment. A model absent from a complete authoritative catalog is rejected
|
|
@@ -1419,7 +1421,7 @@ change files under a directory without prompts.
|
|
|
1419
1421
|
|
|
1420
1422
|
### Which provider can do what
|
|
1421
1423
|
|
|
1422
|
-
| Provider | Supervisor (`brindle --
|
|
1424
|
+
| Provider | Supervisor (`brindle --supervisor …`) | Worker / reviewer |
|
|
1423
1425
|
|---|---|---|
|
|
1424
1426
|
| `claude` (Claude Code) | yes (the default) | yes |
|
|
1425
1427
|
| `codex` | yes | yes |
|
|
@@ -1481,7 +1483,7 @@ When running long, unattended autopilot sessions with API keys:
|
|
|
1481
1483
|
- **Codex (unsupported runtime auth detection)**: Codex CLI turn completion (`agent-turn-complete`) does not classify auth errors at runtime. If an OpenAI API key expires or is rejected, Codex workers will not automatically transition to `failed` or trigger an autopilot credential pause. When running Codex workers unattended, monitor their progress via `brindle watch` or check tmux panes; if an auth failure occurs, manually restart or rotate keys.
|
|
1482
1484
|
- **Supervisor credential refresh and key rotation**:
|
|
1483
1485
|
- **Recovering failed/blocked workers**: When an agent fails on an expired key or autopilot pauses with a credential error, export the fresh key (or update `brindle keys set`), then run `brindle continue`. This unpauses the session and relaunches paused/failed agents with the new credentials. The replacement credentials are automatically synced to the session credential store, dynamically refreshed by the supervisor's launcher on all subsequent worker delegations (`assign`/`handoff`).
|
|
1484
|
-
- **Proactive rotation in live sessions**: Exporting a new key while an active session is healthy does not restart running workers or the supervisor (running `brindle continue` on an active healthy session simply reattaches to tmux without restarting workers). To proactively rotate credentials for the entire session and all running workers: run `brindle stop` -> update credentials in your environment or via `brindle keys set` -> run `brindle continue`.
|
|
1486
|
+
- **Proactive rotation in live sessions**: For Claude keys rotated outside brindle, set Claude Code's `apiKeyHelper` in `~/.claude/settings.json` or the project's `.claude/settings.json`. Claude Code re-runs the helper every 5 minutes (configurable with `CLAUDE_CODE_API_KEY_HELPER_TTL_MS`) and after a 401, so running workers pick up the new key. Don't also export `ANTHROPIC_API_KEY` or set a Claude key with `brindle keys set`, since an environment key takes precedence over the helper. Codex and Antigravity keys still need workers stopped, then `brindle keys set`, then `brindle continue`. Exporting a new key while an active session is healthy does not restart running workers or the supervisor (running `brindle continue` on an active healthy session simply reattaches to tmux without restarting workers). To proactively rotate credentials for the entire session and all running workers: run `brindle stop` -> update credentials in your environment or via `brindle keys set` -> run `brindle continue`.
|
|
1485
1487
|
- **Precedence**: Newly stored keys (`brindle keys set`) or session credentials updated during recovery take effect dynamically for all new panes, overriding stale launcher environments. For profile-level overrides, an explicit profile key (`env.NAME`) takes precedence over default provider credentials.
|
|
1486
1488
|
- **Antigravity permissions prerequisite**: Antigravity (`agy`) does not support approving shell commands solely via external hooks. With `GEMINI_API_KEY`, unattended agy workers will still stall on approval prompts unless permissions are pre-populated in `settings.json` (see Permissions) or run with `agy_approvals: "brindle"` under `permission_policy: "on"` (`--dangerously-skip-permissions`).
|
|
1487
1489
|
|
|
@@ -1966,7 +1968,8 @@ you to sign in. `agy` takes no other key or token from the environment; its
|
|
|
1966
1968
|
Google Cloud, Workforce Identity and Application Default Credentials sign-ins are
|
|
1967
1969
|
chosen on its sign-in screen.
|
|
1968
1970
|
|
|
1969
|
-
Then run the
|
|
1971
|
+
Then run the supervisor on it with `brindle --supervisor antigravity`.
|
|
1972
|
+
Workers keep their own profiles' providers. To mix models,
|
|
1970
1973
|
give a profile `provider: antigravity` (for example a `gemini-reviewer` for a second
|
|
1971
1974
|
model's review) and the supervisor can hand it tasks.
|
|
1972
1975
|
|
|
@@ -63,7 +63,7 @@ uv tool install --editable ~/Projects/brindle # or from a local checkout
|
|
|
63
63
|
brindle drives Claude Code, so you need that too (`npm install -g @anthropic-ai/claude-code`).
|
|
64
64
|
|
|
65
65
|
`brindle --version` prints the installed version; the tmux status bar of every brindle
|
|
66
|
-
session shows it too (`brindle 0.3.
|
|
66
|
+
session shows it too (`brindle 0.3.18`). A session started before an upgrade keeps
|
|
67
67
|
running the old code, and shows the old number, until you restart it.
|
|
68
68
|
|
|
69
69
|
## Quick start
|
|
@@ -384,7 +384,7 @@ knowing them helps when you tell the supervisor how to work.
|
|
|
384
384
|
| `complete_subagent` | supervisor | record the result of a `subagent`-profile task |
|
|
385
385
|
| `set_goal` / `get_progress` / `check_milestone` | supervisor | autopilot's goal, its progress, and running the checks |
|
|
386
386
|
| `record_audit_decision` | supervisor | record acceptance of an audit finding with rationale so subsequent audits respect it |
|
|
387
|
-
| `need_user` | supervisor |
|
|
387
|
+
| `need_user` | supervisor | ask you a question (a decision, a denied action, a stalled external job such as a CI run no runner picks up); only the steps that depend on it wait, the rest keeps going; `answered=true` once you've replied |
|
|
388
388
|
| `transfer_to_repo` | supervisor | move a scratch session's work into a repository |
|
|
389
389
|
| `handover` | supervisor | hand the session to a new supervisor on another branch or worktree, with a note |
|
|
390
390
|
|
|
@@ -610,6 +610,8 @@ wins. When every candidate is out, the repo's `default_agent` runs. The reply sa
|
|
|
610
610
|
what was picked and why, e.g. `weight medium -> developer (Codex at 93%, skipped developer-codex)`.
|
|
611
611
|
An `agent_profile` you pass, or a milestone's `profile`, always wins over weight.
|
|
612
612
|
|
|
613
|
+
The supervisor runs on your Claude Code model. If your organisation's Claude Code managed settings pin the model (e.g. to Sonnet), that applies to the brindle supervisor too. Supervising goes best on Opus (or Fable where available), so if your org allows it, switch in the supervisor's own session with `/model`. Worker profiles set their own model and aren't affected.
|
|
614
|
+
|
|
613
615
|
Provisioning and routing also check explicit models against supported read-only
|
|
614
616
|
catalogs, with the managed model, stored keys and the worker's filtered pane
|
|
615
617
|
environment. A model absent from a complete authoritative catalog is rejected
|
|
@@ -1388,7 +1390,7 @@ change files under a directory without prompts.
|
|
|
1388
1390
|
|
|
1389
1391
|
### Which provider can do what
|
|
1390
1392
|
|
|
1391
|
-
| Provider | Supervisor (`brindle --
|
|
1393
|
+
| Provider | Supervisor (`brindle --supervisor …`) | Worker / reviewer |
|
|
1392
1394
|
|---|---|---|
|
|
1393
1395
|
| `claude` (Claude Code) | yes (the default) | yes |
|
|
1394
1396
|
| `codex` | yes | yes |
|
|
@@ -1450,7 +1452,7 @@ When running long, unattended autopilot sessions with API keys:
|
|
|
1450
1452
|
- **Codex (unsupported runtime auth detection)**: Codex CLI turn completion (`agent-turn-complete`) does not classify auth errors at runtime. If an OpenAI API key expires or is rejected, Codex workers will not automatically transition to `failed` or trigger an autopilot credential pause. When running Codex workers unattended, monitor their progress via `brindle watch` or check tmux panes; if an auth failure occurs, manually restart or rotate keys.
|
|
1451
1453
|
- **Supervisor credential refresh and key rotation**:
|
|
1452
1454
|
- **Recovering failed/blocked workers**: When an agent fails on an expired key or autopilot pauses with a credential error, export the fresh key (or update `brindle keys set`), then run `brindle continue`. This unpauses the session and relaunches paused/failed agents with the new credentials. The replacement credentials are automatically synced to the session credential store, dynamically refreshed by the supervisor's launcher on all subsequent worker delegations (`assign`/`handoff`).
|
|
1453
|
-
- **Proactive rotation in live sessions**: Exporting a new key while an active session is healthy does not restart running workers or the supervisor (running `brindle continue` on an active healthy session simply reattaches to tmux without restarting workers). To proactively rotate credentials for the entire session and all running workers: run `brindle stop` -> update credentials in your environment or via `brindle keys set` -> run `brindle continue`.
|
|
1455
|
+
- **Proactive rotation in live sessions**: For Claude keys rotated outside brindle, set Claude Code's `apiKeyHelper` in `~/.claude/settings.json` or the project's `.claude/settings.json`. Claude Code re-runs the helper every 5 minutes (configurable with `CLAUDE_CODE_API_KEY_HELPER_TTL_MS`) and after a 401, so running workers pick up the new key. Don't also export `ANTHROPIC_API_KEY` or set a Claude key with `brindle keys set`, since an environment key takes precedence over the helper. Codex and Antigravity keys still need workers stopped, then `brindle keys set`, then `brindle continue`. Exporting a new key while an active session is healthy does not restart running workers or the supervisor (running `brindle continue` on an active healthy session simply reattaches to tmux without restarting workers). To proactively rotate credentials for the entire session and all running workers: run `brindle stop` -> update credentials in your environment or via `brindle keys set` -> run `brindle continue`.
|
|
1454
1456
|
- **Precedence**: Newly stored keys (`brindle keys set`) or session credentials updated during recovery take effect dynamically for all new panes, overriding stale launcher environments. For profile-level overrides, an explicit profile key (`env.NAME`) takes precedence over default provider credentials.
|
|
1455
1457
|
- **Antigravity permissions prerequisite**: Antigravity (`agy`) does not support approving shell commands solely via external hooks. With `GEMINI_API_KEY`, unattended agy workers will still stall on approval prompts unless permissions are pre-populated in `settings.json` (see Permissions) or run with `agy_approvals: "brindle"` under `permission_policy: "on"` (`--dangerously-skip-permissions`).
|
|
1456
1458
|
|
|
@@ -1935,7 +1937,8 @@ you to sign in. `agy` takes no other key or token from the environment; its
|
|
|
1935
1937
|
Google Cloud, Workforce Identity and Application Default Credentials sign-ins are
|
|
1936
1938
|
chosen on its sign-in screen.
|
|
1937
1939
|
|
|
1938
|
-
Then run the
|
|
1940
|
+
Then run the supervisor on it with `brindle --supervisor antigravity`.
|
|
1941
|
+
Workers keep their own profiles' providers. To mix models,
|
|
1939
1942
|
give a profile `provider: antigravity` (for example a `gemini-reviewer` for a second
|
|
1940
1943
|
model's review) and the supervisor can hand it tasks.
|
|
1941
1944
|
|
|
@@ -300,9 +300,11 @@ def decorate_worker_prompt(task: str, agent_id: str, ws: Workspace, done_when: s
|
|
|
300
300
|
prompt += PLAN_FIRST_NOTE
|
|
301
301
|
prompt += WORKER_FOOTER.format(agent_id=agent_id, branch=ws.branch,
|
|
302
302
|
guidance=worker_guidance(ws))
|
|
303
|
-
if done_when and provider.name == "claude" and not headless
|
|
304
|
-
|
|
305
|
-
#
|
|
303
|
+
if (done_when and provider.name == "claude" and not headless
|
|
304
|
+
and not provider.hooks_disabled(ws.path)):
|
|
305
|
+
# /goal is an interactive command that Claude Code refuses when hooks
|
|
306
|
+
# are disabled (#133), and headless workers get the finish line above
|
|
307
|
+
# and the Stop hook's reminder to report either way.
|
|
306
308
|
prompt = pilot.worker_goal(prompt, done_when, ws.branch) or prompt
|
|
307
309
|
return prompt
|
|
308
310
|
|
|
@@ -2812,6 +2814,11 @@ def handle_hook(db: DB, agent_id: str, event: str, payload: dict) -> dict | None
|
|
|
2812
2814
|
db.set_status(agent_id, "processing", only_if="waiting")
|
|
2813
2815
|
db.set_status(agent_id, "processing", only_if="idle") # see "pre-tool"
|
|
2814
2816
|
note_permission_outcome(db, agent, payload)
|
|
2817
|
+
from brindle import launch_health
|
|
2818
|
+
try:
|
|
2819
|
+
launch_health.check_mcp(db, agent)
|
|
2820
|
+
except Exception: # noqa: BLE001
|
|
2821
|
+
log.exception("brindle: couldn't check the brindle MCP server's start")
|
|
2815
2822
|
elif event == "stop":
|
|
2816
2823
|
from brindle import inbox, tasks
|
|
2817
2824
|
|
|
@@ -3114,6 +3121,47 @@ def reopen_reviewer(db: DB, agent: Agent, ws: Workspace,
|
|
|
3114
3121
|
return True
|
|
3115
3122
|
|
|
3116
3123
|
|
|
3124
|
+
def review_moot(db: DB, agent: Agent) -> bool:
|
|
3125
|
+
"""A reviewer whose branch already merged at its current commit: there is
|
|
3126
|
+
nothing left for it to review or report. A reviewer of commits made after
|
|
3127
|
+
the merge is not moot."""
|
|
3128
|
+
if agent.mode != "review":
|
|
3129
|
+
return False
|
|
3130
|
+
ws = db.get_workspace(agent.workspace_id)
|
|
3131
|
+
if ws is None:
|
|
3132
|
+
return False
|
|
3133
|
+
from brindle import pipeline
|
|
3134
|
+
|
|
3135
|
+
return (pipeline._settled(db, ws) or "").startswith(pipeline.ALREADY_MERGED)
|
|
3136
|
+
|
|
3137
|
+
|
|
3138
|
+
def liveness(db: DB, a: Agent, ws: Workspace | None, panes: dict[str, bool] | None = None) -> str:
|
|
3139
|
+
"""What brindle can see of an agent for free, as one line for its
|
|
3140
|
+
supervisor: whether it's running, when it last did anything, and what
|
|
3141
|
+
its branch holds, so deciding what to do about it takes no digging.
|
|
3142
|
+
Never raises: a fact it can't read is left out."""
|
|
3143
|
+
parts = []
|
|
3144
|
+
try:
|
|
3145
|
+
parts.append("running" if is_alive(a, panes) else "not running")
|
|
3146
|
+
path = transcript_file(a)
|
|
3147
|
+
last = (os.stat(path).st_mtime if path else
|
|
3148
|
+
tmux.window_activity(a.tmux_window, server=server_of(a)) if a.tmux_window else None)
|
|
3149
|
+
if last:
|
|
3150
|
+
parts[-1] += f", last active {int((time.time() - last) // 60)} min ago"
|
|
3151
|
+
if ws is not None and os.path.isdir(ws.path):
|
|
3152
|
+
base = git.base_ref(ws.path, ws.base_branch or git.default_branch(ws.repo_root))
|
|
3153
|
+
ahead = git.ahead_behind(ws.path, base)[1]
|
|
3154
|
+
stat = git.out(["diff", "--stat", git.merge_base(ws.path, base)], ws.path).splitlines()
|
|
3155
|
+
files = ", ".join(ln.split("|")[0].strip() for ln in stat[:-1][:5])
|
|
3156
|
+
more = f" and {len(stat) - 6} more" if len(stat) > 6 else ""
|
|
3157
|
+
parts.append(f"{ahead} commit(s) ahead of {base}"
|
|
3158
|
+
+ (f" ({files}{more}: {stat[-1].strip()})" if stat else ""))
|
|
3159
|
+
parts.append(f"{len(git.dirty_files(ws.path))} uncommitted file(s)")
|
|
3160
|
+
except Exception: # noqa: BLE001
|
|
3161
|
+
pass
|
|
3162
|
+
return "; ".join(parts)
|
|
3163
|
+
|
|
3164
|
+
|
|
3117
3165
|
def tell_parent_unreported(db: DB, agent: Agent) -> bool:
|
|
3118
3166
|
"""A worker stopped again after being reminded to report, still without a
|
|
3119
3167
|
result. It won't be reminded again on its own, and its supervisor has
|
|
@@ -3126,6 +3174,9 @@ def tell_parent_unreported(db: DB, agent: Agent) -> bool:
|
|
|
3126
3174
|
_note_reviewer_end(db, agent)
|
|
3127
3175
|
return False
|
|
3128
3176
|
ws = db.get_workspace(agent.workspace_id)
|
|
3177
|
+
if review_moot(db, agent):
|
|
3178
|
+
close_later(agent.id)
|
|
3179
|
+
return False
|
|
3129
3180
|
if agent.mode == "review" and ws is not None and os.path.isdir(ws.path):
|
|
3130
3181
|
from brindle import gates
|
|
3131
3182
|
|
|
@@ -3154,9 +3205,10 @@ def tell_parent_unreported(db: DB, agent: Agent) -> bool:
|
|
|
3154
3205
|
if agent.pipeline == "fixing" and agent.result is not None
|
|
3155
3206
|
else f"stopped without calling {tool}")
|
|
3156
3207
|
body = (
|
|
3157
|
-
f"{who} {agent.id}{where} {what}, and won't be reminded "
|
|
3158
|
-
f"
|
|
3159
|
-
f"ask it to finish or report,
|
|
3208
|
+
f"{who} {agent.id}{where} {what}, and won't be reminded again on its own. "
|
|
3209
|
+
f"What brindle sees: {liveness(db, agent, ws)}. Decide from that: send_message to "
|
|
3210
|
+
f"ask it to finish or report, workspace_diff for the full change, or remove_workspace "
|
|
3211
|
+
f"if the work is abandoned."
|
|
3160
3212
|
)
|
|
3161
3213
|
try:
|
|
3162
3214
|
send_message(db, agent.parent_id, body)
|
|
@@ -107,8 +107,11 @@ to be asked each step.
|
|
|
107
107
|
approves. Treat any gaps it finds as unfinished work, and tighten (with
|
|
108
108
|
`set_goal`) the check that let each gap through.
|
|
109
109
|
- Keep going until every milestone passes. If you stop early, brindle will ask
|
|
110
|
-
you to continue.
|
|
111
|
-
|
|
110
|
+
you to continue. A denied action, a stalled external job (a CI run no runner
|
|
111
|
+
picks up) or a question only the user can answer blocks only the steps that
|
|
112
|
+
depend on it: call `need_user` with the question, ask it, keep working the
|
|
113
|
+
rest, and report the blocked item with it. Stop only when nothing else is
|
|
114
|
+
left; once the user answers, call `need_user(answered=true)`.
|
|
112
115
|
- When a milestone passes, tell the user in one line.
|
|
113
116
|
- If a message says autopilot is off, stop driving: wait for the user's
|
|
114
117
|
instructions.
|
|
@@ -632,7 +635,7 @@ def set_enabled(db: DB, root_id: str, on: bool) -> None:
|
|
|
632
635
|
fields: dict[str, object] = {"enabled": int(on), "nudges": 0}
|
|
633
636
|
ap = db.get_autopilot(root_id)
|
|
634
637
|
if on and ap and ap.state in ("blocked", "stalled"):
|
|
635
|
-
fields.update(state="running", note=None)
|
|
638
|
+
fields.update(state="running", note=None, question=None)
|
|
636
639
|
db.update_autopilot(root_id, **fields)
|
|
637
640
|
|
|
638
641
|
|
|
@@ -1043,16 +1046,40 @@ def resolve_profile(db: DB, caller_id: str, repo_root: str, requested: str | Non
|
|
|
1043
1046
|
return choose_profile(db, caller_id, repo_root, requested, task, files)[0]
|
|
1044
1047
|
|
|
1045
1048
|
|
|
1046
|
-
def need_user(db: DB, root_id: str, question: str) -> None:
|
|
1047
|
-
|
|
1049
|
+
def need_user(db: DB, root_id: str, question: str, ask: bool = False) -> None:
|
|
1050
|
+
"""Block autopilot on ``question``. brindle's own blocks (credentials,
|
|
1051
|
+
budget) stop the whole session until the user replies; a supervisor's
|
|
1052
|
+
question (``ask``) holds only the work that depends on it, until the
|
|
1053
|
+
supervisor says it's answered."""
|
|
1054
|
+
note = question.strip()[:500]
|
|
1055
|
+
fields: dict[str, object] = {"state": "blocked", "note": note, "nudges": 0,
|
|
1056
|
+
"question": note if ask else None}
|
|
1057
|
+
if ask:
|
|
1058
|
+
ap = db.get_autopilot(root_id)
|
|
1059
|
+
fields["nudged_at"] = ap.progress if ap else None
|
|
1060
|
+
db.update_autopilot(root_id, **fields)
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
def held(ap: Autopilot) -> bool:
|
|
1064
|
+
"""Autopilot waits on the supervisor's open question, and nothing has
|
|
1065
|
+
blocked it since (that would have replaced the note)."""
|
|
1066
|
+
return ap.state == "blocked" and bool(ap.question) and ap.question == ap.note
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
def answered(db: DB, root_id: str) -> None:
|
|
1070
|
+
db.update_autopilot(root_id, state="running", note=None, question=None, nudges=0)
|
|
1048
1071
|
|
|
1049
1072
|
|
|
1050
1073
|
def user_spoke(db: DB, agent: Agent) -> None:
|
|
1051
1074
|
"""The user typed into the supervisor's chat: whatever blocked autopilot
|
|
1052
|
-
is theirs to have answered, so it may drive again.
|
|
1075
|
+
is theirs to have answered, so it may drive again. An open question stays
|
|
1076
|
+
open (the message may be about something else), but the supervisor gets
|
|
1077
|
+
one more reminder of it."""
|
|
1053
1078
|
ap = db.get_autopilot(agent.id)
|
|
1054
|
-
if ap and ap
|
|
1055
|
-
db.update_autopilot(agent.id,
|
|
1079
|
+
if ap and held(ap):
|
|
1080
|
+
db.update_autopilot(agent.id, nudges=0, nudged_at=None)
|
|
1081
|
+
elif ap and ap.state in ("blocked", "stalled"):
|
|
1082
|
+
db.update_autopilot(agent.id, state="running", note=None, question=None, nudges=0)
|
|
1056
1083
|
elif ap:
|
|
1057
1084
|
db.update_autopilot(agent.id, nudges=0)
|
|
1058
1085
|
|
|
@@ -1476,7 +1503,7 @@ def record_audit(db: DB, root_id: str, approved: bool, summary: str) -> str:
|
|
|
1476
1503
|
|
|
1477
1504
|
# -- progress -----------------------------------------------------------------
|
|
1478
1505
|
|
|
1479
|
-
MARK = {"passed": "✓", "failed": "✗", "pending": "○"}
|
|
1506
|
+
MARK = {"passed": "✓", "failed": "✗", "pending": "○", "working": "◐", "waiting": "◌"}
|
|
1480
1507
|
|
|
1481
1508
|
|
|
1482
1509
|
def counts(db: DB, root_id: str) -> tuple[int, int]:
|
|
@@ -1484,6 +1511,27 @@ def counts(db: DB, root_id: str) -> tuple[int, int]:
|
|
|
1484
1511
|
return sum(m.status == "passed" for m in ms), len(ms)
|
|
1485
1512
|
|
|
1486
1513
|
|
|
1514
|
+
def work_state(db: DB, root_id: str) -> str | None:
|
|
1515
|
+
"""Display-only: "working" or "waiting" while the session has work in
|
|
1516
|
+
flight, else None. A failed milestone's last check is stale then, so it
|
|
1517
|
+
shouldn't read as a plain failure. Never stored on the milestone.
|
|
1518
|
+
|
|
1519
|
+
shortcut: work isn't tied to a milestone, so any in-flight work marks every
|
|
1520
|
+
failed milestone; upgrade if tasks get a milestone link."""
|
|
1521
|
+
from brindle import login_pause
|
|
1522
|
+
|
|
1523
|
+
ap = db.get_autopilot(root_id)
|
|
1524
|
+
if (ap is not None and ap.state == "usage_paused") or login_pause.paused(db, root_id):
|
|
1525
|
+
return "waiting"
|
|
1526
|
+
live = active_workers(db, root_id)
|
|
1527
|
+
if any(a.status != "waiting" for a in live):
|
|
1528
|
+
return "working"
|
|
1529
|
+
if live or any(root_of(db, t.caller_id) == root_id
|
|
1530
|
+
for t in db.list_tasks(state="pending") if t.caller_id):
|
|
1531
|
+
return "waiting"
|
|
1532
|
+
return None
|
|
1533
|
+
|
|
1534
|
+
|
|
1487
1535
|
def progress(db: DB, root_id: str) -> str:
|
|
1488
1536
|
ap = db.get_autopilot(root_id)
|
|
1489
1537
|
if ap is None:
|
|
@@ -1494,12 +1542,16 @@ def progress(db: DB, root_id: str) -> str:
|
|
|
1494
1542
|
"user's request (ask what we're building only if there is no request).")
|
|
1495
1543
|
done, total = counts(db, root_id)
|
|
1496
1544
|
lines = [f"{head}. Goal: {ap.goal}", f"{done} of {total} milestones verified."]
|
|
1497
|
-
|
|
1545
|
+
ms = db.milestones(root_id)
|
|
1546
|
+
w = work_state(db, root_id) if any(m.status == "failed" for m in ms) else None
|
|
1547
|
+
for m in ms:
|
|
1498
1548
|
where = f"@{m.repo}: " if m.repo else ""
|
|
1499
1549
|
how = f"check: `{where}{m.check_cmd}`" if m.check_cmd else "NO CHECK: propose one with set_goal"
|
|
1500
1550
|
when = f", last checked {time.strftime('%H:%M', time.localtime(m.checked_at))}" if m.checked_at else ""
|
|
1501
1551
|
who = f", profile: {m.profile}" if m.profile else ""
|
|
1502
|
-
|
|
1552
|
+
shown = w if m.status == "failed" and w else m.status
|
|
1553
|
+
stale = ", last check failed" if shown != m.status else ""
|
|
1554
|
+
lines.append(f" {MARK.get(shown, '·')} {m.position}. {m.title} ({how}{who}{when}{stale})")
|
|
1503
1555
|
if ap.state in ("blocked", "stalled") and ap.note:
|
|
1504
1556
|
lines.append(f"{ap.state.capitalize()}: {ap.note}")
|
|
1505
1557
|
elif ap.state == "auditing":
|
|
@@ -1526,7 +1578,8 @@ def active_workers(db: DB, root_id: str, *, reviewers: bool = True) -> list[Agen
|
|
|
1526
1578
|
return [a for a in agents.tree(db, root_id)[1:]
|
|
1527
1579
|
if a.mode in agents.REPORTING_MODES and (reviewers or a.mode != "review")
|
|
1528
1580
|
and (a.result is None or a.status in BUSY or a.pipeline)
|
|
1529
|
-
and a.status not in ("paused", "done") and agents.is_alive(a)
|
|
1581
|
+
and a.status not in ("paused", "done") and agents.is_alive(a)
|
|
1582
|
+
and not agents.review_moot(db, a)]
|
|
1530
1583
|
|
|
1531
1584
|
|
|
1532
1585
|
def split_workers(db: DB, root_id: str, *, screen: bool = False) -> tuple[list[Agent], list[Agent]]:
|
|
@@ -1743,6 +1796,9 @@ def on_stop(db: DB, agent: Agent, payload: dict) -> dict | None:
|
|
|
1743
1796
|
return None
|
|
1744
1797
|
else:
|
|
1745
1798
|
return None
|
|
1799
|
+
elif held(ap):
|
|
1800
|
+
if ap.nudged_at == ap.progress:
|
|
1801
|
+
return None # reminded of the open question since the last step or user message
|
|
1746
1802
|
elif ap.state != "running":
|
|
1747
1803
|
return None
|
|
1748
1804
|
|
|
@@ -1763,6 +1819,10 @@ def on_stop(db: DB, agent: Agent, payload: dict) -> dict | None:
|
|
|
1763
1819
|
if any(agents.runs_process(a) for a in working
|
|
1764
1820
|
if getattr(a, "plan_state", None) != "proposed"):
|
|
1765
1821
|
return None # their results arrive as messages and wake it up
|
|
1822
|
+
if held(ap):
|
|
1823
|
+
# One reminder per step: never counts toward stalling.
|
|
1824
|
+
db.update_autopilot(agent.id, nudges=0, nudged_at=ap.progress)
|
|
1825
|
+
return {"decision": "block", "reason": nudge(db, ap, cfg, working, stalled)}
|
|
1766
1826
|
nudges = ap.nudges if ap.nudged_at == ap.progress else 0
|
|
1767
1827
|
if nudges >= MAX_NUDGES:
|
|
1768
1828
|
db.update_autopilot(agent.id, state="stalled", nudges=0,
|
|
@@ -1789,18 +1849,23 @@ def nudge(db: DB, ap: Autopilot, cfg: RepoConfig,
|
|
|
1789
1849
|
"approved=false to ask for changes; the worker is waiting on you.\n\n")
|
|
1790
1850
|
stuck = ""
|
|
1791
1851
|
if stalled:
|
|
1792
|
-
|
|
1852
|
+
seen = "\n".join(f"- {a.id}: {agents.liveness(db, a, db.get_workspace(a.workspace_id))}"
|
|
1853
|
+
for a in stalled)
|
|
1793
1854
|
stuck = (
|
|
1794
|
-
|
|
1795
|
-
"again on their own.
|
|
1796
|
-
"send_message asking them to report or finish,
|
|
1797
|
-
"is abandoned.\n\n"
|
|
1855
|
+
"These went idle without ever calling report_result and won't be reminded "
|
|
1856
|
+
f"again on their own. What brindle sees of each:\n{seen}\nDecide from that: "
|
|
1857
|
+
"send_message asking them to report or finish, workspace_diff for the full change, "
|
|
1858
|
+
"or remove_workspace if the work is abandoned.\n\n"
|
|
1798
1859
|
)
|
|
1799
1860
|
ms = db.milestones(ap.root_id)
|
|
1800
1861
|
if ms and all(m.status == "passed" for m in ms):
|
|
1801
1862
|
pending += ("Every milestone's check passes, but the completion audit hasn't approved "
|
|
1802
1863
|
"the current commit. Close any gaps it found, commit, and call "
|
|
1803
1864
|
"check_milestone: that starts a fresh audit.\n\n")
|
|
1865
|
+
if held(ap):
|
|
1866
|
+
pending += (f"Waiting on the user for: {ap.question}. Work only on what doesn't depend "
|
|
1867
|
+
"on it; if nothing is left, stop. When they've answered, call "
|
|
1868
|
+
"need_user(answered=true).\n\n")
|
|
1804
1869
|
return (
|
|
1805
1870
|
"[brindle autopilot] The goal isn't reached yet, and no workers are running.\n"
|
|
1806
1871
|
f"{progress(db, ap.root_id)}\n\n{pending}{stuck}"
|
|
@@ -9,7 +9,7 @@ import subprocess
|
|
|
9
9
|
import sys
|
|
10
10
|
import time
|
|
11
11
|
from pathlib import Path
|
|
12
|
-
from typing import Optional
|
|
12
|
+
from typing import Annotated, Optional
|
|
13
13
|
|
|
14
14
|
import typer
|
|
15
15
|
|
|
@@ -187,14 +187,18 @@ def new(
|
|
|
187
187
|
pr: Optional[int] = typer.Option(None, "--pr", help="Check out this GitHub PR's head branch (via `gh`), based on the PR's base branch. Don't also pass BRANCH or --base; the head branch is always fetched."),
|
|
188
188
|
no_agent: bool = typer.Option(False, "--no-agent", help="Only create the worktree; start no supervisor."),
|
|
189
189
|
prompt: Optional[str] = typer.Option(None, "--prompt", "-p", help="First message for the supervisor."),
|
|
190
|
-
provider: Optional[str] = typer.Option(
|
|
190
|
+
provider: Optional[str] = typer.Option(
|
|
191
|
+
None, "--supervisor", "-s", help="Run the supervisor on this CLI instead of Claude Code (codex, antigravity). Workers keep their own profiles' providers.",
|
|
192
|
+
),
|
|
191
193
|
no_fetch: bool = typer.Option(False, "--no-fetch", help="Don't fetch the base branch first."),
|
|
192
194
|
no_setup: bool = typer.Option(False, "--no-setup", help="Skip setup commands."),
|
|
193
195
|
attach: bool = typer.Option(False, "--attach", help="Attach to the tmux session afterward."),
|
|
196
|
+
provider_alias: Annotated[Optional[str], typer.Option("--provider", hidden=True)] = None,
|
|
194
197
|
) -> None:
|
|
195
198
|
"""Create a worktree on a new branch and start a supervisor in it."""
|
|
196
199
|
from brindle.providers import NOT_SUPERVISOR, SUPERVISOR_PROVIDERS
|
|
197
200
|
|
|
201
|
+
provider = provider or provider_alias
|
|
198
202
|
if provider and provider in NOT_SUPERVISOR:
|
|
199
203
|
_fail(f"the {provider} provider can't run the supervisor; use one of: "
|
|
200
204
|
f"{', '.join(SUPERVISOR_PROVIDERS)}")
|
|
@@ -246,12 +250,15 @@ def new(
|
|
|
246
250
|
@app.command()
|
|
247
251
|
def start(
|
|
248
252
|
prompt: Optional[str] = typer.Option(None, "--prompt", "-p"),
|
|
249
|
-
provider: Optional[str] = typer.Option(
|
|
253
|
+
provider: Optional[str] = typer.Option(
|
|
254
|
+
None, "--supervisor", "-s", help="Run the supervisor on this CLI instead of Claude Code (codex, antigravity). Workers keep their own profiles' providers.",
|
|
255
|
+
),
|
|
250
256
|
attach: bool = typer.Option(True, "--attach/--no-attach"),
|
|
251
257
|
watch: bool = typer.Option(True, "--watch/--no-watch", help="Show the brindle watch dashboard in a pane under the agent."),
|
|
252
258
|
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)."),
|
|
253
259
|
branch: Optional[str] = typer.Option(None, "--branch", "-b", help="Run in the worktree for this branch, creating the branch and worktree if needed."),
|
|
254
260
|
worktree: Optional[str] = typer.Option(None, "--worktree", "-w", help="Run in the worktree at this path (created with --branch if it doesn't exist)."),
|
|
261
|
+
provider_alias: Annotated[Optional[str], typer.Option("--provider", hidden=True)] = None,
|
|
255
262
|
) -> None:
|
|
256
263
|
"""Start a fresh supervisor chat here, with the dashboard alongside. A launch only ever starts a supervisor; it starts the other agents itself.
|
|
257
264
|
|
|
@@ -263,6 +270,7 @@ def start(
|
|
|
263
270
|
from brindle.config import load_repo_config
|
|
264
271
|
from brindle import scratch
|
|
265
272
|
|
|
273
|
+
provider = provider or provider_alias
|
|
266
274
|
db = DB()
|
|
267
275
|
if branch or worktree:
|
|
268
276
|
ws = _run(workspaces.checkout_for, db, os.getcwd(), branch=branch, worktree=worktree)
|
|
@@ -290,6 +298,14 @@ def start(
|
|
|
290
298
|
typer.secho(f"✓ Switched to '{default_b}'.", fg="green")
|
|
291
299
|
except Exception:
|
|
292
300
|
pass
|
|
301
|
+
elif curr_b and git.upstream_gone(ws.path, curr_b):
|
|
302
|
+
typer.secho(f"! Branch '{curr_b}' was deleted on origin but has unmerged changes.", fg="yellow")
|
|
303
|
+
try:
|
|
304
|
+
if curr_b and git.current_branch(ws.path) == git.default_branch(ws.repo_root):
|
|
305
|
+
if warning := git.fast_forward(ws.path, git.current_branch(ws.path)):
|
|
306
|
+
typer.secho(f"! {warning}", fg="yellow")
|
|
307
|
+
except git.GitError:
|
|
308
|
+
pass
|
|
293
309
|
|
|
294
310
|
from brindle.providers import NOT_SUPERVISOR, SUPERVISOR_PROVIDERS
|
|
295
311
|
|
|
@@ -741,10 +757,14 @@ def default(
|
|
|
741
757
|
ctx: typer.Context,
|
|
742
758
|
cont: bool = typer.Option(False, "--continue", "-c", help="Pick up the most recent paused session instead of starting fresh."),
|
|
743
759
|
autopilot: Optional[bool] = typer.Option(None, "--autopilot/--no-autopilot", help="Start with autopilot on or off (default: on, or `autopilot` in .brindle/config.json)."),
|
|
744
|
-
provider: Optional[str] = typer.Option(
|
|
760
|
+
provider: Optional[str] = typer.Option(
|
|
761
|
+
None, "--supervisor", "-s", help="Run the supervisor on this CLI instead of Claude Code (codex, antigravity). Workers keep their own profiles' providers.",
|
|
762
|
+
),
|
|
745
763
|
show_version: bool = typer.Option(False, "--version", help="Print brindle's version and exit."),
|
|
764
|
+
provider_alias: Annotated[Optional[str], typer.Option("--provider", hidden=True)] = None,
|
|
746
765
|
) -> None:
|
|
747
766
|
"""Bare `brindle`: a fresh supervisor chat here (a scratch session outside git)."""
|
|
767
|
+
provider = provider or provider_alias
|
|
748
768
|
if show_version:
|
|
749
769
|
from brindle import __version__
|
|
750
770
|
|
|
@@ -768,14 +788,18 @@ def default(
|
|
|
768
788
|
@app.command()
|
|
769
789
|
def supervisor(
|
|
770
790
|
prompt: Optional[str] = typer.Option(None, "--prompt", "-p"),
|
|
771
|
-
provider: Optional[str] = typer.Option(
|
|
791
|
+
provider: Optional[str] = typer.Option(
|
|
792
|
+
None, "--supervisor", "-s", help="Run the supervisor on this CLI instead of Claude Code (codex, antigravity). Workers keep their own profiles' providers.",
|
|
793
|
+
),
|
|
772
794
|
attach: bool = typer.Option(True, "--attach/--no-attach"),
|
|
773
795
|
watch: bool = typer.Option(True, "--watch/--no-watch", help="Show the brindle watch dashboard in a pane under the agent."),
|
|
774
796
|
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)."),
|
|
775
797
|
branch: Optional[str] = typer.Option(None, "--branch", "-b", help="Run in the worktree for this branch, creating the branch and worktree if needed."),
|
|
776
798
|
worktree: Optional[str] = typer.Option(None, "--worktree", "-w", help="Run in the worktree at this path (created with --branch if it doesn't exist)."),
|
|
799
|
+
provider_alias: Annotated[Optional[str], typer.Option("--provider", hidden=True)] = None,
|
|
777
800
|
) -> None:
|
|
778
801
|
"""Start an explicit supervisor session to coordinate coding agents."""
|
|
802
|
+
provider = provider or provider_alias
|
|
779
803
|
start(prompt=prompt, provider=provider, attach=attach, watch=watch,
|
|
780
804
|
autopilot=autopilot, branch=branch, worktree=worktree)
|
|
781
805
|
|
|
@@ -278,10 +278,13 @@ class Identity:
|
|
|
278
278
|
billed: str | None = None # who pays for this agent's use, when known
|
|
279
279
|
|
|
280
280
|
|
|
281
|
-
def _unknown(provider: str, tool: str, why: str = "") -> Identity:
|
|
281
|
+
def _unknown(provider: str, tool: str, why: str = "", exe: str = "") -> Identity:
|
|
282
282
|
install = company_login.INSTALL.get(tool)
|
|
283
283
|
if why:
|
|
284
284
|
return Identity(provider, f"{tool}: {why}", determined=False)
|
|
285
|
+
if company_login.which(exe or tool):
|
|
286
|
+
return Identity(provider, f"{tool} is installed but its sign-in check couldn't run, "
|
|
287
|
+
"so the identity can't be checked", determined=False)
|
|
285
288
|
return Identity(provider, f"{tool} isn't installed, so the identity can't be checked"
|
|
286
289
|
+ (f" (install: {install})" if install else ""), determined=False)
|
|
287
290
|
|
|
@@ -306,9 +309,12 @@ def claude_identity(org_id: str, claude, cache: bool = True) -> Identity:
|
|
|
306
309
|
|
|
307
310
|
|
|
308
311
|
def _claude_login(claude, route, cache) -> Identity:
|
|
309
|
-
|
|
312
|
+
from brindle import providers
|
|
313
|
+
|
|
314
|
+
exe = providers.claude_binary()
|
|
315
|
+
got = _probe([exe, "auth", "status"], cache)
|
|
310
316
|
if got is None:
|
|
311
|
-
return _unknown("claude", "claude")
|
|
317
|
+
return _unknown("claude", "claude", exe=exe)
|
|
312
318
|
status = _json(got[1])
|
|
313
319
|
if status is None:
|
|
314
320
|
return Identity("claude", "claude: status unreadable", determined=False)
|
|
@@ -484,7 +484,19 @@ def claude_model(model: str | None, repo_root: str | None = None) -> str | None:
|
|
|
484
484
|
if got is None or got is UNAVAILABLE:
|
|
485
485
|
return model
|
|
486
486
|
c = got[1].claude
|
|
487
|
-
if c is None or not
|
|
487
|
+
if c is None or not route_in_use(got[0], got[1]):
|
|
488
|
+
return model
|
|
489
|
+
return route_model(c, model)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def route_model(c, model: str | None) -> str | None:
|
|
493
|
+
"""The model a Claude pane runs on route ``c`` (a ``ClaudeSetup``) for a
|
|
494
|
+
profile's ``model``: the profile's own when the route serves it, or when
|
|
495
|
+
it is an alias (opus, sonnet, haiku) the route pins in ``models`` (the
|
|
496
|
+
pane's ANTHROPIC_DEFAULT_*_MODEL maps it); else the route's default."""
|
|
497
|
+
if not c.model_ids or model in c.model_ids:
|
|
498
|
+
return model
|
|
499
|
+
if model in ("opus", "sonnet", "haiku") and c.models is not None and getattr(c.models, model):
|
|
488
500
|
return model
|
|
489
501
|
return c.default_model or c.model_ids[0]
|
|
490
502
|
|