brindle 0.3.16__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.16 → brindle-0.3.18}/PKG-INFO +9 -6
- {brindle-0.3.16 → brindle-0.3.18}/README.md +8 -5
- {brindle-0.3.16 → brindle-0.3.18}/pyproject.toml +1 -1
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/agents.py +133 -19
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/autopilot.py +406 -102
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/cli.py +29 -5
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/company_identity.py +9 -3
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/company_login.py +13 -1
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/cull.py +133 -5
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/db.py +36 -5
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/git.py +40 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/history.py +96 -4
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/launch_health.py +77 -4
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/mcp_server.py +31 -14
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pipeline.py +2 -1
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/account.py +6 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/managed_models.py +6 -2
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/team_policy.py +16 -2
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/prompt_answer.py +25 -1
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/providers.py +41 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/sessions.py +13 -3
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/sidebar_follow.py +3 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/usage.py +21 -2
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/view.py +37 -14
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/watch.py +26 -11
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/watch_costs.py +33 -9
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/workspaces.py +15 -4
- {brindle-0.3.16 → brindle-0.3.18}/.gitignore +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/LICENSE +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/SCHEDULE-A +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/__init__.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/__main__.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/account.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/airgap.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/antigravity.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/budget.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/__init__.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/backend-auditor.md +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/developer-codex.md +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/developer-fable.md +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/developer-gemini.md +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/developer-heavy.md +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/developer-light.md +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/developer-local.md +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/developer.md +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/qa.md +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/reviewer-codex.md +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/reviewer-gemini.md +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/reviewer-local.md +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/reviewer.md +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/subagent.md +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/supervisor.md +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_rules/__init__.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_rules/security/backend.md +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_rules/style/minimal-diff.md +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_rules/tests/only.md +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/ci_adapters.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/ci_client.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/ci_federation.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/ci_hosts.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/cloud_credential.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/cloud_login.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/codemap.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/codemap_docs.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/codex_hook.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/config.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/conflicts.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/cost.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/cost_estimate.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/demo.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/detect.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/doctor.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/escapes.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/events.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/gates.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/guardrails.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/identity_lock.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/inbox.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/keys_cmds.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/keystore.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/learned_rules.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/learning.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/legacy.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/login_pause.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/model_access.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/model_availability.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/native/__init__.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/native/client.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/native/loop.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/native/permissions.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/native/runner.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/native/serve.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/native/tools.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pane_auth.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/permissions.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/plugins.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/policy.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pool.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pricing.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/__init__.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/_files.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/audit_chain.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/audit_export.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/auth.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/brief_learning.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/cost_centers.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/credentials.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/features.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/keys.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/learning.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/license.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/loopback.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/org_profiles.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/orgkey.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/rollout.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/route_health.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/settings_sync.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/status.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/team_events.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/procs.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/profiles.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/quota.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/repo_cmds.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/repos.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/rewind.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/rule_checks.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/savings.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/scratch.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/secrets.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/self_serve.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/services.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/signin_pause.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/status_cache.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/tasks.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/tmux.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/updates.py +0 -0
- {brindle-0.3.16 → brindle-0.3.18}/src/brindle/upgrade.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
|
|
|
@@ -322,10 +324,12 @@ def spawn(
|
|
|
322
324
|
autopilot: bool = False,
|
|
323
325
|
plan_first: bool = False,
|
|
324
326
|
review_sha: str | None = None,
|
|
327
|
+
claim_goals: bool = True,
|
|
325
328
|
) -> Agent:
|
|
326
329
|
"""Start an agent in ``ws``. Workers (handoff/assign) given a ``done_when``
|
|
327
330
|
finish line run it as a Claude Code ``/goal``. With ``autopilot``, the
|
|
328
|
-
agent is a session root that drives toward a goal (see brindle.autopilot)
|
|
331
|
+
agent is a session root that drives toward a goal (see brindle.autopilot);
|
|
332
|
+
with ``claim_goals`` it claims a hand-written ``.brindle/goals.md``."""
|
|
329
333
|
from brindle import autopilot as pilot
|
|
330
334
|
from brindle import identity_lock
|
|
331
335
|
|
|
@@ -377,9 +381,9 @@ def spawn(
|
|
|
377
381
|
db.update_agent(agent_id, plan_first=1)
|
|
378
382
|
agent.plan_first = 1
|
|
379
383
|
if autopilot:
|
|
380
|
-
plan = pilot.enable(db, agent_id, ws)
|
|
384
|
+
plan = pilot.enable(db, agent_id, ws, claim=claim_goals)
|
|
381
385
|
if plan and not prompt:
|
|
382
|
-
prompt = agent.task = pilot.kickoff(plan)
|
|
386
|
+
prompt = agent.task = pilot.kickoff(plan, agent_id)
|
|
383
387
|
db.update_agent(agent_id, task=prompt)
|
|
384
388
|
if mode == "interactive":
|
|
385
389
|
from brindle import codemap
|
|
@@ -1177,6 +1181,10 @@ def resume(db: DB, root_id: str, *, watch_pane: bool = True,
|
|
|
1177
1181
|
if a.status in ("paused", "failed") and (only is None or a.id in only)]
|
|
1178
1182
|
for a in candidates: # all or nothing: a session never half-resumes under another identity
|
|
1179
1183
|
identity_lock.ensure(a)
|
|
1184
|
+
if any(a.id == root_id for a in candidates):
|
|
1185
|
+
from brindle import autopilot as pilot
|
|
1186
|
+
|
|
1187
|
+
pilot.reload_goals_file(db, root_id) # edits the person made to this session's goal file
|
|
1180
1188
|
resumed = []
|
|
1181
1189
|
for a in candidates:
|
|
1182
1190
|
if a.status == "failed":
|
|
@@ -1506,14 +1514,22 @@ def screen_status(db: DB, agent: Agent, samples: int = 2, gap: float = 0.7) -> s
|
|
|
1506
1514
|
single cheap read, as the dashboard does, leaves it alone. Moving an
|
|
1507
1515
|
agent to idle also delivers anything queued for it, but only when
|
|
1508
1516
|
``samples`` >= 2: a single-sample read must never pop a message into a
|
|
1509
|
-
terminal as a side effect of just rendering the dashboard.
|
|
1517
|
+
terminal as a side effect of just rendering the dashboard.
|
|
1518
|
+
|
|
1519
|
+
An agent whose CLI runs none of brindle's hooks (``hooks_off``: the
|
|
1520
|
+
person's settings turn them off, at launch or later through /hooks, or
|
|
1521
|
+
none reported within HOOKS_GRACE of its input box appearing) has only the
|
|
1522
|
+
screen to go by: it may leave 'starting', a single read may move it from
|
|
1523
|
+
'idle' to busy, and a ``samples`` >= 2 idle read delivers what's queued
|
|
1524
|
+
even when the status was already idle, since no Stop hook ever will."""
|
|
1510
1525
|
provider = get_provider(agent.provider)
|
|
1511
1526
|
if (agent.headless or not provider.uses_hooks
|
|
1512
1527
|
or agent.status not in ("starting", "idle", "processing", "waiting")):
|
|
1513
1528
|
return None # a headless pane shows output, not a TUI to read
|
|
1514
|
-
|
|
1529
|
+
_sync_hooks_off(db, agent, provider.hooks_disabled(*_settings_scope(db, agent)))
|
|
1530
|
+
if agent.status == "idle" and samples < 2 and not agent.hooks_off:
|
|
1515
1531
|
return None
|
|
1516
|
-
seen = set()
|
|
1532
|
+
seen, ready = set(), True
|
|
1517
1533
|
for i in range(samples):
|
|
1518
1534
|
if i:
|
|
1519
1535
|
time.sleep(gap)
|
|
@@ -1525,25 +1541,71 @@ def screen_status(db: DB, agent: Agent, samples: int = 2, gap: float = 0.7) -> s
|
|
|
1525
1541
|
if agent.status == "idle" and state == "busy" and not provider.busy_in_footer(screen):
|
|
1526
1542
|
state = None
|
|
1527
1543
|
seen.add(state)
|
|
1544
|
+
ready = ready and provider.input_ready(screen)
|
|
1545
|
+
if (agent.status == "starting" and not agent.hooks_off and ready and provider.announces_start
|
|
1546
|
+
and time.time() - (agent.status_since or agent.created_at) >= HOOKS_GRACE):
|
|
1547
|
+
# Its input box is up, and SessionStart, which runs before it, never reported.
|
|
1548
|
+
# (A CLI without one is marked ready by after_launch instead: see ready.)
|
|
1549
|
+
_sync_hooks_off(db, agent, f"{NO_HOOK_EVENTS} from {provider.name}")
|
|
1528
1550
|
if len(seen) != 1:
|
|
1529
1551
|
return None
|
|
1530
1552
|
state = seen.pop()
|
|
1531
1553
|
new = {"idle": "idle", "busy": "processing", "waiting": "waiting"}.get(state or "")
|
|
1532
|
-
if agent.status == "starting" and new != "waiting":
|
|
1554
|
+
if agent.status == "starting" and new != "waiting" and not agent.hooks_off:
|
|
1533
1555
|
# Its hooks say when it's ready; the screen only adds that it's
|
|
1534
1556
|
# stuck on a dialog before they could (e.g. folder trust).
|
|
1535
1557
|
return None
|
|
1536
|
-
|
|
1558
|
+
changed = bool(new) and new != agent.status
|
|
1559
|
+
if changed:
|
|
1537
1560
|
db.set_status(agent.id, new, only_if=agent.status)
|
|
1538
1561
|
agent.status = new
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1562
|
+
if (new == "idle" and samples >= 2 and (changed or agent.hooks_off)
|
|
1563
|
+
and db.pending_count(agent.id)):
|
|
1564
|
+
try:
|
|
1565
|
+
flush(db, agent.id)
|
|
1566
|
+
except tmux.TmuxError:
|
|
1567
|
+
pass # stays queued; its Stop hook hands it over
|
|
1544
1568
|
return new
|
|
1545
1569
|
|
|
1546
1570
|
|
|
1571
|
+
# How long a hooks CLI may show its input box while still 'starting' before
|
|
1572
|
+
# brindle takes it that none of its hooks run (see screen_status).
|
|
1573
|
+
HOOKS_GRACE = 60.0
|
|
1574
|
+
# How hooks_off reads when no settings explain it, only the missing events.
|
|
1575
|
+
NO_HOOK_EVENTS = "no hook events"
|
|
1576
|
+
|
|
1577
|
+
|
|
1578
|
+
_settings_scopes: dict[tuple[str, float], tuple[str | None, list[str] | None]] = {}
|
|
1579
|
+
|
|
1580
|
+
|
|
1581
|
+
def _settings_scope(db: DB, agent: Agent) -> tuple[str | None, list[str] | None]:
|
|
1582
|
+
"""Where ``agent``'s CLI reads its settings: its workspace, and the
|
|
1583
|
+
profile's setting_sources (None: all of them). Kept per agent, as the
|
|
1584
|
+
sidebar asks on every refresh and a running CLI keeps its launch flags."""
|
|
1585
|
+
key = (agent.id, agent.created_at)
|
|
1586
|
+
if key in _settings_scopes:
|
|
1587
|
+
return _settings_scopes[key]
|
|
1588
|
+
from brindle.profiles import ProfileError
|
|
1589
|
+
|
|
1590
|
+
ws = db.get_workspace(agent.workspace_id)
|
|
1591
|
+
try:
|
|
1592
|
+
sources = load_profile(agent.profile, ws.repo_root if ws else None).setting_sources
|
|
1593
|
+
except (KeyError, ProfileError):
|
|
1594
|
+
sources = None
|
|
1595
|
+
scope = _settings_scopes[key] = (ws.path if ws else None), sources
|
|
1596
|
+
return scope
|
|
1597
|
+
|
|
1598
|
+
|
|
1599
|
+
def _sync_hooks_off(db: DB, agent: Agent, why: str | None) -> None:
|
|
1600
|
+
"""Record ``why`` its hooks are off; None clears a reason the settings
|
|
1601
|
+
gave (turned back on), but not missing events, which only a hook clears."""
|
|
1602
|
+
if why is None and agent.hooks_off and agent.hooks_off.startswith(NO_HOOK_EVENTS):
|
|
1603
|
+
return
|
|
1604
|
+
if why != agent.hooks_off:
|
|
1605
|
+
db.update_agent(agent.id, hooks_off=why)
|
|
1606
|
+
agent.hooks_off = why
|
|
1607
|
+
|
|
1608
|
+
|
|
1547
1609
|
def _deliver_to_inbox(db: DB, agent: Agent, message_id: int, text: str,
|
|
1548
1610
|
sender_id: str | None) -> bool:
|
|
1549
1611
|
"""Send a queued message through Claude Code's own inbox (see
|
|
@@ -2661,6 +2723,8 @@ def handle_hook(db: DB, agent_id: str, event: str, payload: dict) -> dict | None
|
|
|
2661
2723
|
agent = db.get_agent(agent_id)
|
|
2662
2724
|
if agent is None:
|
|
2663
2725
|
return None
|
|
2726
|
+
if agent.hooks_off:
|
|
2727
|
+
db.update_agent(agent_id, hooks_off=None) # its hooks run after all: turned back on
|
|
2664
2728
|
# A hook runs only once the session is going, so its launch is past any
|
|
2665
2729
|
# startup prompt: a menu on its pane from now on is the agent's own (a
|
|
2666
2730
|
# question for the user), which no sweep may answer.
|
|
@@ -2750,6 +2814,11 @@ def handle_hook(db: DB, agent_id: str, event: str, payload: dict) -> dict | None
|
|
|
2750
2814
|
db.set_status(agent_id, "processing", only_if="waiting")
|
|
2751
2815
|
db.set_status(agent_id, "processing", only_if="idle") # see "pre-tool"
|
|
2752
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")
|
|
2753
2822
|
elif event == "stop":
|
|
2754
2823
|
from brindle import inbox, tasks
|
|
2755
2824
|
|
|
@@ -3052,6 +3121,47 @@ def reopen_reviewer(db: DB, agent: Agent, ws: Workspace,
|
|
|
3052
3121
|
return True
|
|
3053
3122
|
|
|
3054
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
|
+
|
|
3055
3165
|
def tell_parent_unreported(db: DB, agent: Agent) -> bool:
|
|
3056
3166
|
"""A worker stopped again after being reminded to report, still without a
|
|
3057
3167
|
result. It won't be reminded again on its own, and its supervisor has
|
|
@@ -3064,6 +3174,9 @@ def tell_parent_unreported(db: DB, agent: Agent) -> bool:
|
|
|
3064
3174
|
_note_reviewer_end(db, agent)
|
|
3065
3175
|
return False
|
|
3066
3176
|
ws = db.get_workspace(agent.workspace_id)
|
|
3177
|
+
if review_moot(db, agent):
|
|
3178
|
+
close_later(agent.id)
|
|
3179
|
+
return False
|
|
3067
3180
|
if agent.mode == "review" and ws is not None and os.path.isdir(ws.path):
|
|
3068
3181
|
from brindle import gates
|
|
3069
3182
|
|
|
@@ -3092,9 +3205,10 @@ def tell_parent_unreported(db: DB, agent: Agent) -> bool:
|
|
|
3092
3205
|
if agent.pipeline == "fixing" and agent.result is not None
|
|
3093
3206
|
else f"stopped without calling {tool}")
|
|
3094
3207
|
body = (
|
|
3095
|
-
f"{who} {agent.id}{where} {what}, and won't be reminded "
|
|
3096
|
-
f"
|
|
3097
|
-
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."
|
|
3098
3212
|
)
|
|
3099
3213
|
try:
|
|
3100
3214
|
send_message(db, agent.parent_id, body)
|