brindle 0.3.3__tar.gz → 0.3.4__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.3 → brindle-0.3.4}/.gitignore +2 -0
- {brindle-0.3.3 → brindle-0.3.4}/PKG-INFO +28 -9
- {brindle-0.3.3 → brindle-0.3.4}/README.md +27 -8
- {brindle-0.3.3 → brindle-0.3.4}/pyproject.toml +1 -1
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/agents.py +237 -20
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/antigravity.py +8 -1
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/autopilot.py +324 -40
- brindle-0.3.4/src/brindle/builtin_agents/reviewer-codex.md +30 -0
- brindle-0.3.4/src/brindle/builtin_agents/reviewer-gemini.md +30 -0
- brindle-0.3.4/src/brindle/builtin_agents/reviewer-local.md +29 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/builtin_agents/reviewer.md +8 -3
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/builtin_agents/supervisor.md +6 -4
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/cli.py +112 -15
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/config.py +2 -1
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/db.py +58 -3
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/git.py +68 -4
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/keystore.py +14 -3
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/mcp_server.py +37 -9
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/model_availability.py +5 -2
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/pane_auth.py +5 -4
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/pipeline.py +35 -4
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/providers.py +42 -2
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/sessions.py +2 -2
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/tasks.py +25 -1
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/tmux.py +4 -1
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/view.py +2 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/watch.py +4 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/workspaces.py +70 -3
- brindle-0.3.3/src/brindle/builtin_agents/reviewer-codex.md +0 -23
- brindle-0.3.3/src/brindle/builtin_agents/reviewer-local.md +0 -25
- {brindle-0.3.3 → brindle-0.3.4}/LICENSE +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/SCHEDULE-A +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/__init__.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/__main__.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/account.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/airgap.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/budget.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/builtin_agents/__init__.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/builtin_agents/backend-auditor.md +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/builtin_agents/developer-codex.md +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/builtin_agents/developer-heavy.md +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/builtin_agents/developer-light.md +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/builtin_agents/developer-local.md +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/builtin_agents/developer.md +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/builtin_agents/qa.md +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/builtin_agents/subagent.md +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/builtin_rules/__init__.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/builtin_rules/security/backend.md +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/builtin_rules/style/minimal-diff.md +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/builtin_rules/tests/only.md +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/ci_adapters.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/ci_client.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/ci_federation.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/ci_hosts.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/codemap.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/codex_hook.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/company_identity.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/company_login.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/conflicts.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/cost.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/cost_estimate.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/cull.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/demo.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/detect.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/doctor.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/escapes.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/events.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/gates.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/guardrails.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/history.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/identity_lock.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/inbox.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/keys_cmds.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/launch_health.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/learned_rules.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/learning.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/legacy.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/model_access.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/native/__init__.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/native/client.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/native/loop.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/native/permissions.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/native/runner.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/native/serve.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/native/tools.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/permissions.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/plugins.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/policy.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/pool.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/pricing.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/pro/__init__.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/pro/_files.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/pro/account.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/pro/audit_chain.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/pro/audit_export.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/pro/auth.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/pro/brief_learning.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/pro/cost_centers.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/pro/credentials.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/pro/features.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/pro/keys.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/pro/learning.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/pro/license.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/pro/loopback.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/pro/managed_models.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/pro/org_profiles.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/pro/orgkey.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/pro/rollout.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/pro/settings_sync.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/pro/status.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/pro/team_events.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/pro/team_policy.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/procs.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/profiles.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/quota.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/repo_cmds.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/repos.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/rewind.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/rule_checks.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/savings.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/scratch.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/secrets.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/self_serve.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/services.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/sidebar_follow.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/signin_pause.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/status_cache.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/usage.py +0 -0
- {brindle-0.3.3 → brindle-0.3.4}/src/brindle/watch_costs.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: brindle
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
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.4`). 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
|
|
@@ -118,11 +118,11 @@ a writable home) and, in a repo, what's set up for it, and says what to do about
|
|
|
118
118
|
anything missing. Bare `brindle` runs the essential ones (tmux, the chat's CLI)
|
|
119
119
|
before it launches anything.
|
|
120
120
|
|
|
121
|
-
That's it. `brindle` opens a
|
|
122
|
-
narrow sidebar on the left showing every
|
|
123
|
-
who's
|
|
124
|
-
|
|
125
|
-
branches
|
|
121
|
+
That's it. Bare `brindle` opens a fresh interactive developer chat (Claude Code)
|
|
122
|
+
in your repo (with autopilot off), with a narrow sidebar on the left showing every
|
|
123
|
+
agent: who's working, who's idle, and who's waiting for your approval. To launch an
|
|
124
|
+
autopilot supervisor that splits work between parallel workers, reviews, and merges
|
|
125
|
+
their branches toward verified milestones, run `brindle supervisor`. It starts in under a second.
|
|
126
126
|
|
|
127
127
|
**The sidebar follows you.** There's one sidebar pane per session root, not
|
|
128
128
|
one per window: switch to any other brindle window or session (⏎ in the
|
|
@@ -242,7 +242,7 @@ for mechanical edits and a Codex reviewer for a second model's opinion.
|
|
|
242
242
|
|
|
243
243
|
## Autopilot
|
|
244
244
|
|
|
245
|
-
`brindle` starts the supervisor with autopilot on. Tell it what we're building,
|
|
245
|
+
`brindle supervisor` starts the supervisor with autopilot on (or `brindle start -a supervisor`). Tell it what we're building,
|
|
246
246
|
or put the goal in `.brindle/goals.md`, and it works like a project manager:
|
|
247
247
|
|
|
248
248
|
1. **Goal and milestones.** The goal is split into milestones, and each one has
|
|
@@ -337,7 +337,8 @@ your own status line prints, so what you see doesn't change.
|
|
|
337
337
|
|
|
338
338
|
| | |
|
|
339
339
|
|---|---|
|
|
340
|
-
| `brindle` | a fresh
|
|
340
|
+
| `brindle` | a fresh developer chat here (autopilot off), dashboard alongside |
|
|
341
|
+
| `brindle supervisor` | start an explicit autopilot supervisor chat to coordinate parallel workers and milestones |
|
|
341
342
|
| `brindle init` | detect setup and test commands, write `.brindle/config.json`, check tools |
|
|
342
343
|
| `brindle demo [--local]` | watch brindle finish a tiny practice repo: parallel workers, reviews, gated merges |
|
|
343
344
|
| `brindle new BRANCH [-b BASE] [-a PROFILE] [-p PROMPT]` | worktree + branch + agent |
|
|
@@ -346,6 +347,7 @@ your own status line prints, so what you see doesn't change.
|
|
|
346
347
|
| `brindle start [-a PROFILE] [-p PROMPT] [--no-watch] [--no-autopilot] [-b BRANCH] [-w PATH]` | the same, with options; `-b`/`-w` run it in that branch's worktree (created if needed, or the one you already made), which gets the repo's `.brindle` config |
|
|
347
348
|
| `brindle handover --to BRANCH\|PATH [-n NOTE]` | hand the session to a new supervisor there: goal and milestones, workers, queued tasks and your note move across; the old one is paused |
|
|
348
349
|
| `brindle autopilot [on\|off\|check]` | the goal's progress; turn autopilot on or off; run the checks now |
|
|
350
|
+
| `brindle audit-decision FINDING [-r RATIONALE]` | record acceptance of an audit finding with rationale so subsequent audits respect it |
|
|
349
351
|
| `brindle delegation [conservative\|balanced\|fast]` | how readily the supervisor delegates: fewest tokens, the default, or quickest |
|
|
350
352
|
| `brindle transfer [REPO] [--from SESSION] [-b BRANCH]` | move a scratch session's work into a real repo |
|
|
351
353
|
| `brindle ls [--all]` | workspaces and agents |
|
|
@@ -407,6 +409,7 @@ knowing them helps when you tell the supervisor how to work.
|
|
|
407
409
|
| `approve_plan` | supervisor | approve a worker's plan, or send it back with feedback (`approved=false`) |
|
|
408
410
|
| `complete_subagent` | supervisor | record the result of a `subagent`-profile task |
|
|
409
411
|
| `set_goal` / `get_progress` / `check_milestone` | supervisor | autopilot's goal, its progress, and running the checks |
|
|
412
|
+
| `record_audit_decision` | supervisor | record acceptance of an audit finding with rationale so subsequent audits respect it |
|
|
410
413
|
| `need_user` | supervisor | stop autopilot and ask you a question |
|
|
411
414
|
| `transfer_to_repo` | supervisor | move a scratch session's work into a repository |
|
|
412
415
|
| `handover` | supervisor | hand the session to a new supervisor on another branch or worktree, with a note |
|
|
@@ -1437,6 +1440,22 @@ A profile chooses between the two with `auth`:
|
|
|
1437
1440
|
|
|
1438
1441
|
Your org's `deny_personal_keys` (Enterprise) still applies on top of `auth`.
|
|
1439
1442
|
|
|
1443
|
+
#### Running unattended on API keys
|
|
1444
|
+
|
|
1445
|
+
When running long, unattended autopilot sessions with API keys:
|
|
1446
|
+
|
|
1447
|
+
- **Quota, token and cost measurements per provider**:
|
|
1448
|
+
- Autopilot's automatic usage pause (`usage_limit: 90`) relies on the percentage numbers emitted by Claude Code's status line under subscription plans. Under API-key billing (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`), the CLIs report no subscription percentage, so subscription usage pause thresholds do not trigger; manage spend via provider account budget limits or brindle budget policies.
|
|
1449
|
+
- Token and cost tracking: Token counts and cost estimates are captured from Claude and Codex transcripts. Antigravity (`agy`) currently has **no token/cost measurements**, so budget policies and cost tracking cannot monitor Antigravity spend; enforce usage caps directly in Google AI Studio or your Google Cloud project billing settings.
|
|
1450
|
+
- **Runtime auth failure handling and provider differences**:
|
|
1451
|
+
- **Claude and Antigravity**: Runtime authentication failures (such as 401/403, revoked API keys, or credit/billing errors) are intercepted via StopFailure and exit hooks. Brindle marks the worker as failed, alerts the supervisor, invalidates cached sign-in checks, and pauses autopilot with a `Credentials need attention` notice rather than cycling idle nudges.
|
|
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.
|
|
1453
|
+
- **Supervisor credential refresh and key rotation**:
|
|
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`).
|
|
1455
|
+
- **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`.
|
|
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.
|
|
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`).
|
|
1458
|
+
|
|
1440
1459
|
### Cheap workers
|
|
1441
1460
|
|
|
1442
1461
|
By default a Claude Code worker loads everything your own `claude` does: your
|
|
@@ -59,7 +59,7 @@ uv tool install --editable ~/Projects/brindle # or from a local checkout
|
|
|
59
59
|
brindle drives Claude Code, so you need that too (`npm install -g @anthropic-ai/claude-code`).
|
|
60
60
|
|
|
61
61
|
`brindle --version` prints the installed version; the tmux status bar of every brindle
|
|
62
|
-
session shows it too (`brindle 0.3.
|
|
62
|
+
session shows it too (`brindle 0.3.4`). 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
|
|
@@ -88,11 +88,11 @@ a writable home) and, in a repo, what's set up for it, and says what to do about
|
|
|
88
88
|
anything missing. Bare `brindle` runs the essential ones (tmux, the chat's CLI)
|
|
89
89
|
before it launches anything.
|
|
90
90
|
|
|
91
|
-
That's it. `brindle` opens a
|
|
92
|
-
narrow sidebar on the left showing every
|
|
93
|
-
who's
|
|
94
|
-
|
|
95
|
-
branches
|
|
91
|
+
That's it. Bare `brindle` opens a fresh interactive developer chat (Claude Code)
|
|
92
|
+
in your repo (with autopilot off), with a narrow sidebar on the left showing every
|
|
93
|
+
agent: who's working, who's idle, and who's waiting for your approval. To launch an
|
|
94
|
+
autopilot supervisor that splits work between parallel workers, reviews, and merges
|
|
95
|
+
their branches toward verified milestones, run `brindle supervisor`. It starts in under a second.
|
|
96
96
|
|
|
97
97
|
**The sidebar follows you.** There's one sidebar pane per session root, not
|
|
98
98
|
one per window: switch to any other brindle window or session (⏎ in the
|
|
@@ -212,7 +212,7 @@ for mechanical edits and a Codex reviewer for a second model's opinion.
|
|
|
212
212
|
|
|
213
213
|
## Autopilot
|
|
214
214
|
|
|
215
|
-
`brindle` starts the supervisor with autopilot on. Tell it what we're building,
|
|
215
|
+
`brindle supervisor` starts the supervisor with autopilot on (or `brindle start -a supervisor`). Tell it what we're building,
|
|
216
216
|
or put the goal in `.brindle/goals.md`, and it works like a project manager:
|
|
217
217
|
|
|
218
218
|
1. **Goal and milestones.** The goal is split into milestones, and each one has
|
|
@@ -307,7 +307,8 @@ your own status line prints, so what you see doesn't change.
|
|
|
307
307
|
|
|
308
308
|
| | |
|
|
309
309
|
|---|---|
|
|
310
|
-
| `brindle` | a fresh
|
|
310
|
+
| `brindle` | a fresh developer chat here (autopilot off), dashboard alongside |
|
|
311
|
+
| `brindle supervisor` | start an explicit autopilot supervisor chat to coordinate parallel workers and milestones |
|
|
311
312
|
| `brindle init` | detect setup and test commands, write `.brindle/config.json`, check tools |
|
|
312
313
|
| `brindle demo [--local]` | watch brindle finish a tiny practice repo: parallel workers, reviews, gated merges |
|
|
313
314
|
| `brindle new BRANCH [-b BASE] [-a PROFILE] [-p PROMPT]` | worktree + branch + agent |
|
|
@@ -316,6 +317,7 @@ your own status line prints, so what you see doesn't change.
|
|
|
316
317
|
| `brindle start [-a PROFILE] [-p PROMPT] [--no-watch] [--no-autopilot] [-b BRANCH] [-w PATH]` | the same, with options; `-b`/`-w` run it in that branch's worktree (created if needed, or the one you already made), which gets the repo's `.brindle` config |
|
|
317
318
|
| `brindle handover --to BRANCH\|PATH [-n NOTE]` | hand the session to a new supervisor there: goal and milestones, workers, queued tasks and your note move across; the old one is paused |
|
|
318
319
|
| `brindle autopilot [on\|off\|check]` | the goal's progress; turn autopilot on or off; run the checks now |
|
|
320
|
+
| `brindle audit-decision FINDING [-r RATIONALE]` | record acceptance of an audit finding with rationale so subsequent audits respect it |
|
|
319
321
|
| `brindle delegation [conservative\|balanced\|fast]` | how readily the supervisor delegates: fewest tokens, the default, or quickest |
|
|
320
322
|
| `brindle transfer [REPO] [--from SESSION] [-b BRANCH]` | move a scratch session's work into a real repo |
|
|
321
323
|
| `brindle ls [--all]` | workspaces and agents |
|
|
@@ -377,6 +379,7 @@ knowing them helps when you tell the supervisor how to work.
|
|
|
377
379
|
| `approve_plan` | supervisor | approve a worker's plan, or send it back with feedback (`approved=false`) |
|
|
378
380
|
| `complete_subagent` | supervisor | record the result of a `subagent`-profile task |
|
|
379
381
|
| `set_goal` / `get_progress` / `check_milestone` | supervisor | autopilot's goal, its progress, and running the checks |
|
|
382
|
+
| `record_audit_decision` | supervisor | record acceptance of an audit finding with rationale so subsequent audits respect it |
|
|
380
383
|
| `need_user` | supervisor | stop autopilot and ask you a question |
|
|
381
384
|
| `transfer_to_repo` | supervisor | move a scratch session's work into a repository |
|
|
382
385
|
| `handover` | supervisor | hand the session to a new supervisor on another branch or worktree, with a note |
|
|
@@ -1407,6 +1410,22 @@ A profile chooses between the two with `auth`:
|
|
|
1407
1410
|
|
|
1408
1411
|
Your org's `deny_personal_keys` (Enterprise) still applies on top of `auth`.
|
|
1409
1412
|
|
|
1413
|
+
#### Running unattended on API keys
|
|
1414
|
+
|
|
1415
|
+
When running long, unattended autopilot sessions with API keys:
|
|
1416
|
+
|
|
1417
|
+
- **Quota, token and cost measurements per provider**:
|
|
1418
|
+
- Autopilot's automatic usage pause (`usage_limit: 90`) relies on the percentage numbers emitted by Claude Code's status line under subscription plans. Under API-key billing (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`), the CLIs report no subscription percentage, so subscription usage pause thresholds do not trigger; manage spend via provider account budget limits or brindle budget policies.
|
|
1419
|
+
- Token and cost tracking: Token counts and cost estimates are captured from Claude and Codex transcripts. Antigravity (`agy`) currently has **no token/cost measurements**, so budget policies and cost tracking cannot monitor Antigravity spend; enforce usage caps directly in Google AI Studio or your Google Cloud project billing settings.
|
|
1420
|
+
- **Runtime auth failure handling and provider differences**:
|
|
1421
|
+
- **Claude and Antigravity**: Runtime authentication failures (such as 401/403, revoked API keys, or credit/billing errors) are intercepted via StopFailure and exit hooks. Brindle marks the worker as failed, alerts the supervisor, invalidates cached sign-in checks, and pauses autopilot with a `Credentials need attention` notice rather than cycling idle nudges.
|
|
1422
|
+
- **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.
|
|
1423
|
+
- **Supervisor credential refresh and key rotation**:
|
|
1424
|
+
- **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`).
|
|
1425
|
+
- **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`.
|
|
1426
|
+
- **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.
|
|
1427
|
+
- **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`).
|
|
1428
|
+
|
|
1410
1429
|
### Cheap workers
|
|
1411
1430
|
|
|
1412
1431
|
By default a Claude Code worker loads everything your own `claude` does: your
|
|
@@ -190,13 +190,55 @@ def agent_env(ws: Workspace, agent_id: str, agent: Agent | None = None,
|
|
|
190
190
|
company = {} if agent.provider != "claude" else company_login.stored_env()
|
|
191
191
|
managed_env = {} if org else managed_models.agent_env(m, agent.provider)
|
|
192
192
|
enforced = None if agent.provider != "claude" or org else company_identity.enforced_route_env()
|
|
193
|
+
session_creds = {}
|
|
194
|
+
try:
|
|
195
|
+
from brindle.db import DB
|
|
196
|
+
db_inst = DB()
|
|
197
|
+
root_id = agent.parent_id or agent_id if agent else agent_id
|
|
198
|
+
curr_id = root_id
|
|
199
|
+
while curr_id:
|
|
200
|
+
ca = db_inst.get_agent(curr_id)
|
|
201
|
+
if ca and ca.parent_id:
|
|
202
|
+
curr_id = ca.parent_id
|
|
203
|
+
else:
|
|
204
|
+
break
|
|
205
|
+
session_creds = db_inst.get_session_credentials_with_timestamps(curr_id or root_id)
|
|
206
|
+
except Exception:
|
|
207
|
+
pass
|
|
208
|
+
|
|
209
|
+
from brindle import keystore, secrets
|
|
210
|
+
guarded = _guarded_profile(agent, ws)
|
|
211
|
+
org_managed = agent.provider == "claude" and bool(providers.claude_org_managed())
|
|
212
|
+
deny = pane_auth.deny_names(agent.provider, getattr(guarded, "auth", "auto") if guarded is not None else "auto",
|
|
213
|
+
m, org_managed)
|
|
214
|
+
no_stored = set(deny) | (set(providers._ENV_AUTH["claude"]) if org_managed else set())
|
|
215
|
+
|
|
216
|
+
creds = set() if org else secrets.agent_credentials(agent.provider, agent.profile, ws.repo_root)
|
|
217
|
+
recovery_keys = {}
|
|
218
|
+
for k in creds:
|
|
219
|
+
if k in no_stored:
|
|
220
|
+
continue
|
|
221
|
+
sess_val, sess_time = session_creds.get(k, (None, 0.0))
|
|
222
|
+
env_val = os.environ.get(k)
|
|
223
|
+
if env_val and not secrets.is_job_secret(k) and agent and agent.status == "failed":
|
|
224
|
+
recovery_keys[k] = env_val
|
|
225
|
+
elif sess_val:
|
|
226
|
+
try:
|
|
227
|
+
ks_val, ks_time = keystore.get_key_info(k)
|
|
228
|
+
except Exception:
|
|
229
|
+
ks_val, ks_time = None, 0.0
|
|
230
|
+
if ks_val and ks_time > sess_time:
|
|
231
|
+
recovery_keys[k] = ks_val
|
|
232
|
+
else:
|
|
233
|
+
recovery_keys[k] = sess_val
|
|
234
|
+
|
|
193
235
|
if enforced is None:
|
|
194
|
-
env = {**company, **profile_env, **managed_env, **env}
|
|
236
|
+
env = {**company, **recovery_keys, **profile_env, **managed_env, **env}
|
|
195
237
|
else:
|
|
196
238
|
# An enforced company identity (Enterprise): the pane runs on the route
|
|
197
239
|
# launch_problem checked, whatever a profile's env lines say.
|
|
198
240
|
profile_env = {k: v for k, v in profile_env.items() if company_identity.profile_may_set(k)}
|
|
199
|
-
env = {**profile_env, **managed_env, **enforced, **env}
|
|
241
|
+
env = {**recovery_keys, **profile_env, **managed_env, **enforced, **env}
|
|
200
242
|
if agent is not None and preload_tools(agent, ws):
|
|
201
243
|
# Claude Code defers MCP tools and loads them on demand, which costs a
|
|
202
244
|
# worker an extra round trip at the moment it's told to report (and
|
|
@@ -717,8 +759,12 @@ def _launch(db: DB, agent: Agent, ws: Workspace, *, prompt: str | None,
|
|
|
717
759
|
if why:
|
|
718
760
|
raise AgentError(why)
|
|
719
761
|
# The org's own key signs the agent in: there is no login to check.
|
|
762
|
+
prov_creds = set() if org_key else secrets.agent_credentials(provider.name, profile.name, ws.repo_root)
|
|
763
|
+
eff_env = _window_environment(agent, ws, m)[0]
|
|
764
|
+
probe_keys = {k: eff_env[k] for k in prov_creds if k in eff_env}
|
|
765
|
+
probe_env = {**launch_env, **probe_keys}
|
|
720
766
|
why = None if org_key else signed_out(
|
|
721
|
-
provider.name, {**
|
|
767
|
+
provider.name, {**probe_env, **{k: "" for k in secrets.pane_deny(deny, probe_env)}})
|
|
722
768
|
if why:
|
|
723
769
|
raise AgentError(why)
|
|
724
770
|
# Here rather than in spawn, so a resume checks too: a directory can be
|
|
@@ -1044,19 +1090,61 @@ def latest_paused(db: DB, ws: Workspace) -> Agent | None:
|
|
|
1044
1090
|
|
|
1045
1091
|
|
|
1046
1092
|
def resume(db: DB, root_id: str, *, watch_pane: bool = True,
|
|
1047
|
-
only: set[str] | None = None
|
|
1093
|
+
only: set[str] | list[str] | None = None,
|
|
1094
|
+
recovery_env: dict[str, str] | None = None) -> list[Agent]:
|
|
1048
1095
|
"""Bring a paused session back: the supervisor and its paused workers
|
|
1049
1096
|
restart in their own workspaces. Claude Code picks up its previous
|
|
1050
1097
|
conversation (--resume); other CLIs restart on their original task.
|
|
1051
1098
|
With ``only``, just those agents (if paused) restart."""
|
|
1052
|
-
from brindle import identity_lock
|
|
1099
|
+
from brindle import identity_lock, providers
|
|
1100
|
+
|
|
1101
|
+
providers.invalidate_signed_in()
|
|
1102
|
+
from brindle import keystore, secrets
|
|
1103
|
+
|
|
1104
|
+
# Explicit recovery replacements (from recovery_env or current process environment)
|
|
1105
|
+
env_keys = recovery_env if recovery_env is not None else {
|
|
1106
|
+
k: os.environ[k] for k in keystore.known_names()
|
|
1107
|
+
if k in os.environ and os.environ[k] and not secrets.is_job_secret(k)
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
session_creds = db.get_session_credentials_with_timestamps(root_id)
|
|
1111
|
+
for k in keystore.known_names():
|
|
1112
|
+
try:
|
|
1113
|
+
ks_val, ks_time = keystore.get_key_info(k)
|
|
1114
|
+
except Exception:
|
|
1115
|
+
ks_val, ks_time = None, 0.0
|
|
1116
|
+
sess_val, sess_time = session_creds.get(k, (None, 0.0))
|
|
1117
|
+
env_val = env_keys.get(k)
|
|
1118
|
+
|
|
1119
|
+
val = None
|
|
1120
|
+
if ks_val and (ks_time > sess_time or (sess_val and env_val == sess_val and ks_val != sess_val) or not env_val):
|
|
1121
|
+
val = ks_val
|
|
1122
|
+
elif env_val:
|
|
1123
|
+
val = env_val
|
|
1124
|
+
elif ks_val:
|
|
1125
|
+
val = ks_val
|
|
1126
|
+
|
|
1127
|
+
if val:
|
|
1128
|
+
db.set_session_credential(root_id, k, val)
|
|
1129
|
+
os.environ[k] = val
|
|
1053
1130
|
|
|
1054
1131
|
candidates = [a for a in tree(db, root_id)
|
|
1055
|
-
if a.status
|
|
1132
|
+
if a.status in ("paused", "failed") and (only is None or a.id in only)]
|
|
1056
1133
|
for a in candidates: # all or nothing: a session never half-resumes under another identity
|
|
1057
1134
|
identity_lock.ensure(a)
|
|
1058
1135
|
resumed = []
|
|
1059
1136
|
for a in candidates:
|
|
1137
|
+
if a.status == "failed":
|
|
1138
|
+
if a.tmux_window and same_server(a) and owns_pane(db, a):
|
|
1139
|
+
try:
|
|
1140
|
+
tmux.kill_window(a.tmux_window)
|
|
1141
|
+
except Exception:
|
|
1142
|
+
pass
|
|
1143
|
+
from brindle import procs
|
|
1144
|
+
try:
|
|
1145
|
+
procs.stop([a.id], grace=2.0)
|
|
1146
|
+
except Exception:
|
|
1147
|
+
continue
|
|
1060
1148
|
ws = db.get_workspace(a.workspace_id)
|
|
1061
1149
|
if ws is None or not os.path.isdir(ws.path):
|
|
1062
1150
|
continue
|
|
@@ -1081,6 +1169,14 @@ def resume(db: DB, root_id: str, *, watch_pane: bool = True,
|
|
|
1081
1169
|
_launch(db, a, ws, prompt=prompt, resume=ref,
|
|
1082
1170
|
watch_pane=watch_pane and a.id == root_id, background_setup=True)
|
|
1083
1171
|
resumed.append(a)
|
|
1172
|
+
ap = db.get_autopilot(root_id)
|
|
1173
|
+
failed_still = [a for a in candidates if db.get_agent(a.id) and db.get_agent(a.id).status == "failed"]
|
|
1174
|
+
if ap and ap.state == "blocked" and resumed and not failed_still:
|
|
1175
|
+
note = (ap.note or "").lower()
|
|
1176
|
+
auth_indicators = ("auth", "credential", "sign in", "signed out", "login", "api key", "key", "token", "unauthorized", "forbidden")
|
|
1177
|
+
if any(ind in note for ind in auth_indicators):
|
|
1178
|
+
db.update_autopilot(root_id, state="running", note=None)
|
|
1179
|
+
|
|
1084
1180
|
return resumed
|
|
1085
1181
|
|
|
1086
1182
|
|
|
@@ -1642,8 +1738,9 @@ def detach(db: DB, agent_id: str) -> str | None:
|
|
|
1642
1738
|
"""Stop waiting synchronously on a handoff worker: from now on its result
|
|
1643
1739
|
is forwarded to its parent as a message. Returns the result instead if it
|
|
1644
1740
|
arrived in the meantime (so it's never lost between the two paths)."""
|
|
1645
|
-
db.update_agent(agent_id, mode="handoff_detached")
|
|
1646
1741
|
agent = db.get_agent(agent_id)
|
|
1742
|
+
if agent and agent.mode == "handoff":
|
|
1743
|
+
db.update_agent(agent_id, mode="handoff_detached")
|
|
1647
1744
|
return agent.result if agent else None
|
|
1648
1745
|
|
|
1649
1746
|
|
|
@@ -1662,11 +1759,12 @@ def submit_review(db: DB, caller_id: str, approved: bool, summary: str) -> str:
|
|
|
1662
1759
|
|
|
1663
1760
|
caller = db.get_agent(caller_id)
|
|
1664
1761
|
ws = db.get_workspace(caller.workspace_id) if caller else None
|
|
1665
|
-
|
|
1762
|
+
root_id = autopilot.root_of(db, caller.id) if caller else None
|
|
1763
|
+
ap = db.get_autopilot(root_id) if root_id else None
|
|
1764
|
+
is_auditor = bool(ap and ap.auditor_id == caller_id)
|
|
1765
|
+
if not caller or (caller.mode != "review" and not is_auditor) or ws is None:
|
|
1666
1766
|
return "Only a reviewer started with request_review can submit a review."
|
|
1667
|
-
|
|
1668
|
-
ap = db.get_autopilot(root_id)
|
|
1669
|
-
if ap and ap.auditor_id == caller.id: # the completion audit, not a branch review
|
|
1767
|
+
if is_auditor: # the completion audit, not a branch review
|
|
1670
1768
|
text = autopilot.record_audit(db, root_id, approved, summary)
|
|
1671
1769
|
report_result(db, caller.id, text, forward=False)
|
|
1672
1770
|
try:
|
|
@@ -1879,6 +1977,14 @@ def default_review_profile(cfg: RepoConfig, worker: Agent | None, db: DB | None
|
|
|
1879
1977
|
model on its own: a Claude worker reviewed by a Claude reviewer is fine."""
|
|
1880
1978
|
if cfg.review_profile:
|
|
1881
1979
|
return cfg.review_profile
|
|
1980
|
+
if cfg.reviewer and cfg.reviewer != "reviewer":
|
|
1981
|
+
return cfg.reviewer
|
|
1982
|
+
if unusable("antigravity") is None:
|
|
1983
|
+
try:
|
|
1984
|
+
load_profile("reviewer-gemini", repo_root)
|
|
1985
|
+
return "reviewer-gemini"
|
|
1986
|
+
except Exception:
|
|
1987
|
+
pass
|
|
1882
1988
|
if db is None or not repo_root:
|
|
1883
1989
|
return cfg.reviewer
|
|
1884
1990
|
from brindle import learning
|
|
@@ -1886,8 +1992,18 @@ def default_review_profile(cfg: RepoConfig, worker: Agent | None, db: DB | None
|
|
|
1886
1992
|
if learning.plugin(cfg, repo_root) is None:
|
|
1887
1993
|
return cfg.reviewer # no learner: don't even probe the alternatives
|
|
1888
1994
|
candidates = [cfg.reviewer]
|
|
1995
|
+
if unusable("antigravity") is None:
|
|
1996
|
+
try:
|
|
1997
|
+
load_profile("reviewer-gemini", repo_root)
|
|
1998
|
+
candidates.append("reviewer-gemini")
|
|
1999
|
+
except Exception:
|
|
2000
|
+
pass
|
|
1889
2001
|
if unusable("codex") is None:
|
|
1890
|
-
|
|
2002
|
+
try:
|
|
2003
|
+
load_profile("reviewer-codex", repo_root)
|
|
2004
|
+
candidates.append("reviewer-codex")
|
|
2005
|
+
except Exception:
|
|
2006
|
+
pass
|
|
1891
2007
|
if _local_reviewer_available():
|
|
1892
2008
|
candidates.append("reviewer-local")
|
|
1893
2009
|
candidates = list(dict.fromkeys(candidates))
|
|
@@ -2062,10 +2178,52 @@ def request_review(db: DB, caller: Agent | None, ws: Workspace, profile: str | N
|
|
|
2062
2178
|
"it isn't pre-approved). "
|
|
2063
2179
|
"Look for correctness bugs, missing tests, security problems and unclear code.")
|
|
2064
2180
|
|
|
2181
|
+
worker_files = []
|
|
2182
|
+
worker_done_when = getattr(worker, "done_when", None) if worker else None
|
|
2183
|
+
if worker:
|
|
2184
|
+
if getattr(worker, "files", None):
|
|
2185
|
+
worker_files = worker.files
|
|
2186
|
+
else:
|
|
2187
|
+
t = next((t for t in db.list_tasks(ws.repo_root) if t.agent_id == worker.id), None)
|
|
2188
|
+
if t:
|
|
2189
|
+
if t.files:
|
|
2190
|
+
import json
|
|
2191
|
+
try:
|
|
2192
|
+
worker_files = json.loads(t.files)
|
|
2193
|
+
except Exception:
|
|
2194
|
+
worker_files = [t.files]
|
|
2195
|
+
if not worker_done_when and t.done_when:
|
|
2196
|
+
worker_done_when = t.done_when
|
|
2065
2197
|
if worker and worker.task:
|
|
2066
|
-
task += f"\n\nThe worker's original task:\n{worker.task.strip()
|
|
2067
|
-
if worker
|
|
2068
|
-
task += f"\n\
|
|
2198
|
+
task += f"\n\nThe worker's original task:\n{worker.task.strip()}"
|
|
2199
|
+
if worker:
|
|
2200
|
+
task += f"\n\nWorker profile: {worker.profile} (provider: {worker.provider})"
|
|
2201
|
+
try:
|
|
2202
|
+
from brindle.profiles import load_profile
|
|
2203
|
+
wp = load_profile(worker.profile, ws.repo_root)
|
|
2204
|
+
restrictions = []
|
|
2205
|
+
if wp.allowed_tools:
|
|
2206
|
+
restrictions.append(f"allowed_tools: {', '.join(wp.allowed_tools)}")
|
|
2207
|
+
if wp.permission_mode:
|
|
2208
|
+
restrictions.append(f"permission_mode: {wp.permission_mode}")
|
|
2209
|
+
if wp.permission_denies:
|
|
2210
|
+
restrictions.append(f"permission_denies: {', '.join(wp.permission_denies)}")
|
|
2211
|
+
if getattr(wp, "write_scope", None):
|
|
2212
|
+
restrictions.append(f"write_scope: {', '.join(wp.write_scope)}")
|
|
2213
|
+
if getattr(wp, "read_scope", None):
|
|
2214
|
+
restrictions.append(f"read_scope: {', '.join(wp.read_scope)}")
|
|
2215
|
+
if getattr(wp, "env_allow", None):
|
|
2216
|
+
restrictions.append(f"env_allow: {', '.join(wp.env_allow)}")
|
|
2217
|
+
if wp.strict_mcp:
|
|
2218
|
+
restrictions.append("strict_mcp: true")
|
|
2219
|
+
if restrictions:
|
|
2220
|
+
task += f"\nWorker restrictions:\n" + "\n".join(f"- {r}" for r in restrictions)
|
|
2221
|
+
except Exception:
|
|
2222
|
+
pass
|
|
2223
|
+
if worker_files:
|
|
2224
|
+
task += f"\n\nDeclared files: {', '.join(worker_files)}"
|
|
2225
|
+
if worker_done_when:
|
|
2226
|
+
task += f"\n\nIts finish line: {worker_done_when.strip()}"
|
|
2069
2227
|
|
|
2070
2228
|
if check_summary:
|
|
2071
2229
|
task += (f"\n\nThe repo's checks have already run on this commit:\n\n{check_summary}\n\n"
|
|
@@ -2085,6 +2243,19 @@ def request_review(db: DB, caller: Agent | None, ws: Workspace, profile: str | N
|
|
|
2085
2243
|
|
|
2086
2244
|
if focus:
|
|
2087
2245
|
task += f"\n\nFocus: {focus}"
|
|
2246
|
+
|
|
2247
|
+
task += (
|
|
2248
|
+
"\n\nClassify each finding into one of:\n"
|
|
2249
|
+
"- `blocker`: must be fixed before the branch can merge.\n"
|
|
2250
|
+
"- `minor`: style nits, minor comments or suggestions that do not block merging.\n"
|
|
2251
|
+
"- `needs supervisor action: <details>`: an issue the worker cannot fix within its allowed tools or permissions (e.g. network access, elevated migrations, scope decisions).\n\n"
|
|
2252
|
+
"Check whether your findings are actionable by the worker: if a fix requires capabilities outside the worker's brief, flag it as `needs supervisor action`. Check the finish line (`done_when`) for substance, not just placeholder presence (e.g. data populated, not merely keys existing)."
|
|
2253
|
+
)
|
|
2254
|
+
if check_summary:
|
|
2255
|
+
task += " If you judged checks without running Bash, state clearly which checks you ran and which you read from history."
|
|
2256
|
+
task += (
|
|
2257
|
+
"\n\nApprove (`approved=true`) only if there are no blockers and no supervisor-action items. If only minor findings remain, approve and list them under minor findings."
|
|
2258
|
+
)
|
|
2088
2259
|
return spawn(db, ws, profile, prompt=task, parent_id=caller.id if caller else None,
|
|
2089
2260
|
mode="review", background_setup=True, review_sha=sha)
|
|
2090
2261
|
|
|
@@ -2477,12 +2648,58 @@ def handle_hook(db: DB, agent_id: str, event: str, payload: dict) -> dict | None
|
|
|
2477
2648
|
flush(db, agent_id)
|
|
2478
2649
|
elif event == "stop-failure":
|
|
2479
2650
|
# The turn ended on an API error; no Stop hook follows.
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2651
|
+
err_fields = " ".join(
|
|
2652
|
+
str(payload.get(k) or "") for k in (
|
|
2653
|
+
"error", "error_type", "error_details", "last_assistant_message",
|
|
2654
|
+
"message", "reason", "details", "status", "status_code", "code"
|
|
2655
|
+
)
|
|
2656
|
+
).lower()
|
|
2657
|
+
is_rate_limit = bool(re.search(r"\b(429|rate_limit_error|rate_limit|rate-limit|overloaded|resource_exhausted)\b", err_fields))
|
|
2658
|
+
auth_enums = {
|
|
2659
|
+
"oauth_org_not_allowed", "billing_error", "authentication_error",
|
|
2660
|
+
"permission_error", "invalid_api_key", "account_suspended", "revoked_api_key"
|
|
2661
|
+
}
|
|
2662
|
+
is_auth = not is_rate_limit and (
|
|
2663
|
+
payload.get("error_type") == "auth"
|
|
2664
|
+
or str(payload.get("error") or "").lower() in auth_enums
|
|
2665
|
+
or str(payload.get("error_type") or "").lower() in auth_enums
|
|
2666
|
+
or bool(re.search(r"\b(401|403|oauth_org_not_allowed|billing_error)\b", err_fields))
|
|
2667
|
+
or any(w in err_fields for w in (
|
|
2668
|
+
"unauthorized", "authentication", "auth_fail", "auth fail",
|
|
2669
|
+
"invalid api key", "invalid_api_key", "expired token", "expired_token",
|
|
2670
|
+
"credit balance", "credit-balance", "revoked", "forbidden",
|
|
2671
|
+
"oauth_org_not_allowed", "billing_error"
|
|
2672
|
+
))
|
|
2673
|
+
)
|
|
2674
|
+
if is_auth:
|
|
2675
|
+
db.set_status(agent_id, "failed")
|
|
2676
|
+
db.clear_permission_requests(agent_id)
|
|
2677
|
+
from brindle import providers, autopilot as pilot
|
|
2678
|
+
providers.invalidate_signed_in(agent.provider)
|
|
2679
|
+
err_desc = payload.get("error_details") or payload.get("error") or payload.get("error_type") or "401/403"
|
|
2680
|
+
msg = (
|
|
2681
|
+
f"[brindle] Agent {agent_id} encountered an authentication failure "
|
|
2682
|
+
f"(invalid or expired credentials): {err_desc}. "
|
|
2683
|
+
"Check API keys or login status."
|
|
2684
|
+
)
|
|
2685
|
+
if agent.parent_id:
|
|
2686
|
+
try:
|
|
2687
|
+
send_message(db, agent.parent_id, msg, sender_id="brindle")
|
|
2688
|
+
except Exception:
|
|
2689
|
+
try:
|
|
2690
|
+
db.enqueue(agent.parent_id, msg, "brindle")
|
|
2691
|
+
except Exception:
|
|
2692
|
+
pass
|
|
2693
|
+
root_id = pilot.root_of(db, agent_id)
|
|
2694
|
+
if db.get_autopilot(root_id):
|
|
2695
|
+
pilot.need_user(db, root_id, f"Credentials need attention for agent {agent_id}: {err_desc}")
|
|
2696
|
+
else:
|
|
2697
|
+
db.set_status(agent_id, "idle")
|
|
2698
|
+
db.clear_permission_requests(agent_id)
|
|
2699
|
+
if is_rate_limit:
|
|
2700
|
+
from brindle import autopilot as pilot
|
|
2484
2701
|
|
|
2485
|
-
|
|
2702
|
+
pilot.limit_reached(db, agent)
|
|
2486
2703
|
elif event == "subagent-start":
|
|
2487
2704
|
# A crash can skip SubagentStop, so this doesn't touch agent.status:
|
|
2488
2705
|
# the sidebar hides a subagent that's been "running" too long instead.
|
|
@@ -254,6 +254,11 @@ def agent_from_parent(max_depth: int = 4) -> str | None:
|
|
|
254
254
|
# -- hooks ---------------------------------------------------------------------------
|
|
255
255
|
|
|
256
256
|
LIMIT_ERROR = re.compile(r"429|quota|rate.?limit|resource.?exhausted", re.I)
|
|
257
|
+
AUTH_ERROR = re.compile(
|
|
258
|
+
r"401|403|unauthorized|authentication|auth[-_ ]?fail|invalid[-_ ]?(?:api[-_ ]?)?key|"
|
|
259
|
+
r"expired[-_ ]?(?:token|key)?|credit[-_ ]?balance|billing|permission denied",
|
|
260
|
+
re.I,
|
|
261
|
+
)
|
|
257
262
|
|
|
258
263
|
|
|
259
264
|
def handle_hook(db, agent_id: str, event: str, payload: dict) -> dict | None:
|
|
@@ -283,14 +288,16 @@ def handle_hook(db, agent_id: str, event: str, payload: dict) -> dict | None:
|
|
|
283
288
|
error = str(payload.get("error") or "")
|
|
284
289
|
if error:
|
|
285
290
|
limited = bool(LIMIT_ERROR.search(error))
|
|
291
|
+
autherr = bool(AUTH_ERROR.search(error))
|
|
286
292
|
if limited:
|
|
287
293
|
from brindle import quota
|
|
288
294
|
from brindle.config import RepoConfig, load_repo_config
|
|
289
295
|
|
|
290
296
|
ws = db.get_workspace(agent.workspace_id)
|
|
291
297
|
quota.record_limit("antigravity", load_repo_config(ws.repo_root) if ws else RepoConfig())
|
|
298
|
+
error_type = "rate_limit" if limited else ("auth" if autherr else "error")
|
|
292
299
|
agents.handle_hook(db, agent_id, "stop-failure",
|
|
293
|
-
{**base, "error_type": "
|
|
300
|
+
{**base, "error_type": error_type, "error": error})
|
|
294
301
|
return None
|
|
295
302
|
if db.pending_count(agent_id):
|
|
296
303
|
# A new turn, not a continuation: only then does agy give the
|