brindle 0.3.11__tar.gz → 0.3.13__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.11 → brindle-0.3.13}/PKG-INFO +56 -4
- {brindle-0.3.11 → brindle-0.3.13}/README.md +55 -3
- {brindle-0.3.11 → brindle-0.3.13}/pyproject.toml +1 -1
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/agents.py +142 -47
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/antigravity.py +54 -8
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/autopilot.py +3 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/cli.py +4 -1
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/cloud_credential.py +24 -4
- brindle-0.3.13/src/brindle/codemap.py +422 -0
- brindle-0.3.13/src/brindle/codemap_docs.py +65 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/company_login.py +35 -8
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/cost.py +17 -2
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/cull.py +64 -6
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/db.py +24 -2
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/doctor.py +29 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/events.py +29 -6
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/history.py +1 -1
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/launch_health.py +8 -3
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/learning.py +14 -10
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/native/permissions.py +113 -14
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/permissions.py +70 -12
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pipeline.py +60 -6
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/learning.py +11 -9
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/managed_models.py +22 -4
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/route_health.py +5 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/settings_sync.py +5 -4
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/team_policy.py +26 -3
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/providers.py +24 -2
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/usage.py +44 -7
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/workspaces.py +20 -7
- brindle-0.3.11/src/brindle/codemap.py +0 -73
- {brindle-0.3.11 → brindle-0.3.13}/.gitignore +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/LICENSE +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/SCHEDULE-A +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/__init__.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/__main__.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/account.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/airgap.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/budget.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/__init__.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/backend-auditor.md +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/developer-codex.md +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/developer-fable.md +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/developer-gemini.md +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/developer-heavy.md +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/developer-light.md +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/developer-local.md +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/developer.md +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/qa.md +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/reviewer-codex.md +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/reviewer-gemini.md +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/reviewer-local.md +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/reviewer.md +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/subagent.md +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/supervisor.md +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_rules/__init__.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_rules/security/backend.md +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_rules/style/minimal-diff.md +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_rules/tests/only.md +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/ci_adapters.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/ci_client.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/ci_federation.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/ci_hosts.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/codex_hook.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/company_identity.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/config.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/conflicts.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/cost_estimate.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/demo.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/detect.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/escapes.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/gates.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/git.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/guardrails.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/identity_lock.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/inbox.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/keys_cmds.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/keystore.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/learned_rules.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/legacy.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/mcp_server.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/model_access.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/model_availability.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/native/__init__.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/native/client.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/native/loop.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/native/runner.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/native/serve.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/native/tools.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pane_auth.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/plugins.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/policy.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pool.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pricing.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/__init__.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/_files.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/account.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/audit_chain.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/audit_export.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/auth.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/brief_learning.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/cost_centers.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/credentials.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/features.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/keys.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/license.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/loopback.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/org_profiles.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/orgkey.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/rollout.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/status.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/team_events.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/procs.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/profiles.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/quota.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/repo_cmds.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/repos.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/rewind.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/rule_checks.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/savings.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/scratch.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/secrets.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/self_serve.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/services.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/sessions.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/sidebar_follow.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/signin_pause.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/status_cache.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/tasks.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/tmux.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/updates.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/view.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/src/brindle/watch.py +0 -0
- {brindle-0.3.11 → brindle-0.3.13}/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.13
|
|
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.13`). 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
|
|
@@ -1865,12 +1865,64 @@ documents its own quirks (no prompt caching on most, smaller context windows),
|
|
|
1865
1865
|
so prefer the native provider for open-weight models and keep this route for
|
|
1866
1866
|
Claude itself behind a gateway.
|
|
1867
1867
|
|
|
1868
|
+
To run workers on your own Bedrock, Vertex, Codex or Antigravity account, see
|
|
1869
|
+
[Running workers on your own cloud](#running-workers-on-your-own-cloud) below.
|
|
1870
|
+
|
|
1868
1871
|
Codex agents report status through Codex's `notify` hook (brindle passes
|
|
1869
1872
|
`-c notify=[...]` at launch, leaving your own Codex config alone): a completed turn
|
|
1870
1873
|
marks the agent idle and delivers any queued message. Codex has no Stop hook, so for
|
|
1871
1874
|
an autopilot supervisor on Codex that turn end is also where brindle tells it to keep
|
|
1872
1875
|
going, with the same limit on reminders as Claude Code.
|
|
1873
1876
|
|
|
1877
|
+
### Running workers on your own cloud
|
|
1878
|
+
|
|
1879
|
+
Any brindle user, free tier included, can run a worker's model on their own cloud
|
|
1880
|
+
account. A profile's `env.NAME: value` lines are set in the agent's environment
|
|
1881
|
+
before brindle's own variables, so the profile decides which backend the worker
|
|
1882
|
+
uses. Profiles live in `.brindle/agents/` in the repo, or in your user profiles
|
|
1883
|
+
folder; `brindle profile new` writes one.
|
|
1884
|
+
|
|
1885
|
+
```markdown
|
|
1886
|
+
---
|
|
1887
|
+
name: developer-bedrock
|
|
1888
|
+
provider: claude
|
|
1889
|
+
model: us.anthropic.claude-haiku-4-5-20251001-v1:0 # a Bedrock model id your account can use
|
|
1890
|
+
env.CLAUDE_CODE_USE_BEDROCK: 1
|
|
1891
|
+
env.AWS_REGION: us-east-1
|
|
1892
|
+
---
|
|
1893
|
+
```
|
|
1894
|
+
|
|
1895
|
+
Pass `developer-bedrock` as the worker profile (`agent_profile` in `assign`), or
|
|
1896
|
+
list it under `routing` in `.brindle/config.json`.
|
|
1897
|
+
|
|
1898
|
+
- **Claude Code on Bedrock.** Set `env.CLAUDE_CODE_USE_BEDROCK: 1` and
|
|
1899
|
+
`env.AWS_REGION`. The worker uses the AWS credentials on this machine (an
|
|
1900
|
+
`AWS_PROFILE` from `~/.aws/config`, SSO or environment keys), so sign in to AWS
|
|
1901
|
+
first. See [Claude Code on Amazon Bedrock](https://code.claude.com/docs/en/amazon-bedrock).
|
|
1902
|
+
- **Claude Code on Vertex AI.** Set `env.CLAUDE_CODE_USE_VERTEX: 1`,
|
|
1903
|
+
`env.CLOUD_ML_REGION` and `env.ANTHROPIC_VERTEX_PROJECT_ID`. The worker
|
|
1904
|
+
authenticates with Application Default Credentials, so run
|
|
1905
|
+
`gcloud auth application-default login` once. See
|
|
1906
|
+
[Claude Code on Google Vertex AI](https://code.claude.com/docs/en/google-vertex-ai).
|
|
1907
|
+
- **Codex on Bedrock.** Codex picks its provider in `config.toml` in its home
|
|
1908
|
+
folder (`$CODEX_HOME`, default `~/.codex`). Point that file at the
|
|
1909
|
+
`amazon-bedrock` provider, as described in
|
|
1910
|
+
[Codex advanced configuration](https://learn.chatgpt.com/docs/config-file/config-advanced).
|
|
1911
|
+
To give one profile its own Codex home, add `env.CODEX_HOME: /path/to/home`
|
|
1912
|
+
to it. Each Codex home keeps its own sign-in (`auth.json`).
|
|
1913
|
+
- **Google Antigravity.** `agy` can run on a Gemini API key: set
|
|
1914
|
+
`"modelProvider": "gemini"` in `~/.gemini/antigravity-cli/settings.json`, and
|
|
1915
|
+
export `GEMINI_API_KEY` in your shell, or put it in a profile in your user
|
|
1916
|
+
profiles folder; never in a profile under `.brindle/agents/`, which is committed. For a
|
|
1917
|
+
Gemini-compatible gateway, set `env.GOOGLE_GEMINI_BASE_URL` to its URL. As of
|
|
1918
|
+
1.3.4, `agy` ignores Vertex AI credentials, so use a Gemini API key or gateway.
|
|
1919
|
+
The full setup is in [Google Antigravity](#google-antigravity) below.
|
|
1920
|
+
|
|
1921
|
+
If your org runs brindle for a team, its admins can set a route for everyone in
|
|
1922
|
+
the org's Routing settings on the brindle dashboard; an enforced org route replaces
|
|
1923
|
+
a profile's `env.` lines, so a profile can't move a worker off the company's route. See
|
|
1924
|
+
[Using your company's access](#using-your-companys-access).
|
|
1925
|
+
|
|
1874
1926
|
### Using your company's access
|
|
1875
1927
|
|
|
1876
1928
|
Two ways to run brindle's agents on your company's Claude (Bedrock, Vertex,
|
|
@@ -1908,8 +1960,8 @@ agy # sign in with your Google account, then quit
|
|
|
1908
1960
|
To run unattended workers without a browser login, `agy` (1.1.13 or newer) can use
|
|
1909
1961
|
a Gemini API key instead: put `"modelProvider": "gemini"` in
|
|
1910
1962
|
`~/.gemini/antigravity-cli/settings.json` and set `GEMINI_API_KEY`, in your
|
|
1911
|
-
environment or in a profile's `env.GEMINI_API_KEY: ...` line
|
|
1912
|
-
|
|
1963
|
+
environment or in a profile's `env.GEMINI_API_KEY: ...` line in your user
|
|
1964
|
+
profiles folder (never in `.brindle/agents/`, which is committed). The key alone does nothing: without `modelProvider`, `agy` still asks
|
|
1913
1965
|
you to sign in. `agy` takes no other key or token from the environment; its
|
|
1914
1966
|
Google Cloud, Workforce Identity and Application Default Credentials sign-ins are
|
|
1915
1967
|
chosen on its sign-in screen.
|
|
@@ -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.13`). 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
|
|
@@ -1834,12 +1834,64 @@ documents its own quirks (no prompt caching on most, smaller context windows),
|
|
|
1834
1834
|
so prefer the native provider for open-weight models and keep this route for
|
|
1835
1835
|
Claude itself behind a gateway.
|
|
1836
1836
|
|
|
1837
|
+
To run workers on your own Bedrock, Vertex, Codex or Antigravity account, see
|
|
1838
|
+
[Running workers on your own cloud](#running-workers-on-your-own-cloud) below.
|
|
1839
|
+
|
|
1837
1840
|
Codex agents report status through Codex's `notify` hook (brindle passes
|
|
1838
1841
|
`-c notify=[...]` at launch, leaving your own Codex config alone): a completed turn
|
|
1839
1842
|
marks the agent idle and delivers any queued message. Codex has no Stop hook, so for
|
|
1840
1843
|
an autopilot supervisor on Codex that turn end is also where brindle tells it to keep
|
|
1841
1844
|
going, with the same limit on reminders as Claude Code.
|
|
1842
1845
|
|
|
1846
|
+
### Running workers on your own cloud
|
|
1847
|
+
|
|
1848
|
+
Any brindle user, free tier included, can run a worker's model on their own cloud
|
|
1849
|
+
account. A profile's `env.NAME: value` lines are set in the agent's environment
|
|
1850
|
+
before brindle's own variables, so the profile decides which backend the worker
|
|
1851
|
+
uses. Profiles live in `.brindle/agents/` in the repo, or in your user profiles
|
|
1852
|
+
folder; `brindle profile new` writes one.
|
|
1853
|
+
|
|
1854
|
+
```markdown
|
|
1855
|
+
---
|
|
1856
|
+
name: developer-bedrock
|
|
1857
|
+
provider: claude
|
|
1858
|
+
model: us.anthropic.claude-haiku-4-5-20251001-v1:0 # a Bedrock model id your account can use
|
|
1859
|
+
env.CLAUDE_CODE_USE_BEDROCK: 1
|
|
1860
|
+
env.AWS_REGION: us-east-1
|
|
1861
|
+
---
|
|
1862
|
+
```
|
|
1863
|
+
|
|
1864
|
+
Pass `developer-bedrock` as the worker profile (`agent_profile` in `assign`), or
|
|
1865
|
+
list it under `routing` in `.brindle/config.json`.
|
|
1866
|
+
|
|
1867
|
+
- **Claude Code on Bedrock.** Set `env.CLAUDE_CODE_USE_BEDROCK: 1` and
|
|
1868
|
+
`env.AWS_REGION`. The worker uses the AWS credentials on this machine (an
|
|
1869
|
+
`AWS_PROFILE` from `~/.aws/config`, SSO or environment keys), so sign in to AWS
|
|
1870
|
+
first. See [Claude Code on Amazon Bedrock](https://code.claude.com/docs/en/amazon-bedrock).
|
|
1871
|
+
- **Claude Code on Vertex AI.** Set `env.CLAUDE_CODE_USE_VERTEX: 1`,
|
|
1872
|
+
`env.CLOUD_ML_REGION` and `env.ANTHROPIC_VERTEX_PROJECT_ID`. The worker
|
|
1873
|
+
authenticates with Application Default Credentials, so run
|
|
1874
|
+
`gcloud auth application-default login` once. See
|
|
1875
|
+
[Claude Code on Google Vertex AI](https://code.claude.com/docs/en/google-vertex-ai).
|
|
1876
|
+
- **Codex on Bedrock.** Codex picks its provider in `config.toml` in its home
|
|
1877
|
+
folder (`$CODEX_HOME`, default `~/.codex`). Point that file at the
|
|
1878
|
+
`amazon-bedrock` provider, as described in
|
|
1879
|
+
[Codex advanced configuration](https://learn.chatgpt.com/docs/config-file/config-advanced).
|
|
1880
|
+
To give one profile its own Codex home, add `env.CODEX_HOME: /path/to/home`
|
|
1881
|
+
to it. Each Codex home keeps its own sign-in (`auth.json`).
|
|
1882
|
+
- **Google Antigravity.** `agy` can run on a Gemini API key: set
|
|
1883
|
+
`"modelProvider": "gemini"` in `~/.gemini/antigravity-cli/settings.json`, and
|
|
1884
|
+
export `GEMINI_API_KEY` in your shell, or put it in a profile in your user
|
|
1885
|
+
profiles folder; never in a profile under `.brindle/agents/`, which is committed. For a
|
|
1886
|
+
Gemini-compatible gateway, set `env.GOOGLE_GEMINI_BASE_URL` to its URL. As of
|
|
1887
|
+
1.3.4, `agy` ignores Vertex AI credentials, so use a Gemini API key or gateway.
|
|
1888
|
+
The full setup is in [Google Antigravity](#google-antigravity) below.
|
|
1889
|
+
|
|
1890
|
+
If your org runs brindle for a team, its admins can set a route for everyone in
|
|
1891
|
+
the org's Routing settings on the brindle dashboard; an enforced org route replaces
|
|
1892
|
+
a profile's `env.` lines, so a profile can't move a worker off the company's route. See
|
|
1893
|
+
[Using your company's access](#using-your-companys-access).
|
|
1894
|
+
|
|
1843
1895
|
### Using your company's access
|
|
1844
1896
|
|
|
1845
1897
|
Two ways to run brindle's agents on your company's Claude (Bedrock, Vertex,
|
|
@@ -1877,8 +1929,8 @@ agy # sign in with your Google account, then quit
|
|
|
1877
1929
|
To run unattended workers without a browser login, `agy` (1.1.13 or newer) can use
|
|
1878
1930
|
a Gemini API key instead: put `"modelProvider": "gemini"` in
|
|
1879
1931
|
`~/.gemini/antigravity-cli/settings.json` and set `GEMINI_API_KEY`, in your
|
|
1880
|
-
environment or in a profile's `env.GEMINI_API_KEY: ...` line
|
|
1881
|
-
|
|
1932
|
+
environment or in a profile's `env.GEMINI_API_KEY: ...` line in your user
|
|
1933
|
+
profiles folder (never in `.brindle/agents/`, which is committed). The key alone does nothing: without `modelProvider`, `agy` still asks
|
|
1882
1934
|
you to sign in. `agy` takes no other key or token from the environment; its
|
|
1883
1935
|
Google Cloud, Workforce Identity and Application Default Credentials sign-ins are
|
|
1884
1936
|
chosen on its sign-in screen.
|
|
@@ -148,7 +148,7 @@ def worker_guidance(ws: Workspace) -> str:
|
|
|
148
148
|
checks = load_repo_config(ws.repo_root).checks
|
|
149
149
|
except ValueError:
|
|
150
150
|
checks = []
|
|
151
|
-
parts = (codemap.guidance(ws
|
|
151
|
+
parts = (codemap.guidance(ws), test_guidance(checks),
|
|
152
152
|
PERMISSION_GUIDANCE.format(root=ws.repo_root))
|
|
153
153
|
return "\n".join(p for p in parts if p)
|
|
154
154
|
|
|
@@ -379,6 +379,9 @@ def spawn(
|
|
|
379
379
|
if plan and not prompt:
|
|
380
380
|
prompt = agent.task = pilot.kickoff(plan)
|
|
381
381
|
db.update_agent(agent_id, task=prompt)
|
|
382
|
+
if mode == "interactive":
|
|
383
|
+
from brindle import codemap
|
|
384
|
+
codemap.seed_session(ws.path, ws.repo_root)
|
|
382
385
|
try:
|
|
383
386
|
_launch(db, agent, ws, prompt=prompt, resume=None, watch_pane=watch_pane,
|
|
384
387
|
background_setup=background_setup, model_checked=True)
|
|
@@ -449,9 +452,12 @@ def _profile_for(db: DB, agent: Agent, ws: Workspace, m=_UNREAD):
|
|
|
449
452
|
if agent.mode == "interactive":
|
|
450
453
|
from brindle import codemap, repos
|
|
451
454
|
|
|
452
|
-
note = codemap.guidance(ws
|
|
455
|
+
note = codemap.guidance(ws)
|
|
453
456
|
if note:
|
|
454
457
|
profile = replace(profile, prompt=f"{profile.prompt}\n\n{note}".strip())
|
|
458
|
+
repo_map = codemap.supervisor_map(ws) if autopilot_on or agent.profile == "supervisor" else None
|
|
459
|
+
if repo_map:
|
|
460
|
+
profile = replace(profile, prompt=f"{profile.prompt}\n\n{repo_map}".strip())
|
|
455
461
|
attached = repos.guidance(db, agent.id) # a session root's attached repos
|
|
456
462
|
if attached:
|
|
457
463
|
profile = replace(profile, prompt=f"{profile.prompt}\n\n{attached}".strip())
|
|
@@ -811,14 +817,12 @@ def _launch(db: DB, agent: Agent, ws: Workspace, *, prompt: str | None,
|
|
|
811
817
|
_launch_headless(db, agent, ws, prompt=prompt, resume=resume, watch_pane=watch_pane, m=m)
|
|
812
818
|
return
|
|
813
819
|
if provider.prompt_after_ready:
|
|
814
|
-
#
|
|
815
|
-
#
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
if prompt:
|
|
821
|
-
db.enqueue(agent.id, prompt, None)
|
|
820
|
+
# Typed in once it's ready, after a warm-up carrying its instructions:
|
|
821
|
+
# agy connects its MCP servers only once a conversation has started,
|
|
822
|
+
# so the task must arrive as a second turn (see brindle.antigravity).
|
|
823
|
+
for text in ((provider.warmup(profile) if not resume else None), prompt):
|
|
824
|
+
if text:
|
|
825
|
+
db.enqueue(agent.id, text, None)
|
|
822
826
|
prompt = None
|
|
823
827
|
# Recorded before launching: the agent's hooks may fire within milliseconds.
|
|
824
828
|
status = "processing" if prompt else "starting"
|
|
@@ -831,6 +835,11 @@ def _launch(db: DB, agent: Agent, ws: Workspace, *, prompt: str | None,
|
|
|
831
835
|
from brindle.providers import trust_folder
|
|
832
836
|
|
|
833
837
|
trust_folder(ws.path) # no trust dialog for nobody to answer
|
|
838
|
+
if provider.name == "antigravity" and m is not None and m.setup is not None and m.setup.antigravity is not None:
|
|
839
|
+
try:
|
|
840
|
+
providers.set_agy_gemini()
|
|
841
|
+
except (OSError, ValueError) as e:
|
|
842
|
+
raise AgentError(f"couldn't set Antigravity to the company Gemini route in its settings.json: {e}") from e
|
|
834
843
|
argv = provider.command(LaunchContext(agent.id, profile, prompt, resume=resume, cwd=ws.path,
|
|
835
844
|
mode=agent.mode, plan_first=bool(agent.plan_first)))
|
|
836
845
|
if prompt or db.pending_count(agent.id):
|
|
@@ -1675,6 +1684,7 @@ def report_result(db: DB, agent_id: str, result: str, forward: bool = True,
|
|
|
1675
1684
|
else:
|
|
1676
1685
|
agent = get(db, agent_id)
|
|
1677
1686
|
ws = db.get_workspace(agent.workspace_id)
|
|
1687
|
+
first = agent.result is None
|
|
1678
1688
|
db.set_result(agent.id, result)
|
|
1679
1689
|
# Usage and history are extras: never let them stop the result arriving.
|
|
1680
1690
|
try:
|
|
@@ -1701,6 +1711,8 @@ def report_result(db: DB, agent_id: str, result: str, forward: bool = True,
|
|
|
1701
1711
|
else:
|
|
1702
1712
|
forwarded = result
|
|
1703
1713
|
if ws and agent.mode in ("handoff", "handoff_detached", "assign"):
|
|
1714
|
+
if first:
|
|
1715
|
+
pipeline.note_reported(db, ws, agent)
|
|
1704
1716
|
warm_checks(ws)
|
|
1705
1717
|
if forward and pipeline.on_report(db, agent, ws, forwarded):
|
|
1706
1718
|
return ("result recorded. brindle is having your branch reviewed; it will merge it, "
|
|
@@ -2011,12 +2023,6 @@ def default_review_profile(cfg: RepoConfig, worker: Agent | None, db: DB | None
|
|
|
2011
2023
|
return cfg.review_profile
|
|
2012
2024
|
if cfg.reviewer and cfg.reviewer != "reviewer":
|
|
2013
2025
|
return cfg.reviewer
|
|
2014
|
-
if unusable("antigravity") is None:
|
|
2015
|
-
try:
|
|
2016
|
-
load_profile("reviewer-gemini", repo_root)
|
|
2017
|
-
return "reviewer-gemini"
|
|
2018
|
-
except Exception:
|
|
2019
|
-
pass
|
|
2020
2026
|
if db is None or not repo_root:
|
|
2021
2027
|
return cfg.reviewer
|
|
2022
2028
|
from brindle import learning
|
|
@@ -2065,6 +2071,15 @@ def _review_kill_switch(ws: Workspace) -> None:
|
|
|
2065
2071
|
raise AgentError(f"No reviewer started: {why_not}")
|
|
2066
2072
|
|
|
2067
2073
|
|
|
2074
|
+
def _review_worktree_present(ws: Workspace) -> None:
|
|
2075
|
+
"""Raises ``AgentError`` when ``ws``'s worktree is gone from disk (removed
|
|
2076
|
+
outside brindle, so its record outlived it): every git call on the branch
|
|
2077
|
+
would fail there, and nothing is left to review."""
|
|
2078
|
+
if not os.path.isdir(ws.path):
|
|
2079
|
+
raise AgentError(f"No reviewer started: {ws.id}'s worktree {ws.path} no longer exists. "
|
|
2080
|
+
f"remove_workspace {ws.id} clears its record.")
|
|
2081
|
+
|
|
2082
|
+
|
|
2068
2083
|
def _review_profile(db: DB, ws: Workspace, profile: str | None, cfg: RepoConfig) -> str:
|
|
2069
2084
|
"""The reviewer profile to use, checked to be one that can run: raises
|
|
2070
2085
|
``AgentError`` if it (explicit or picked) doesn't exist, or uses the codex
|
|
@@ -2130,6 +2145,7 @@ def start_review(db: DB, caller: Agent | None, ws: Workspace, profile: str | Non
|
|
|
2130
2145
|
from brindle.config import load_repo_config
|
|
2131
2146
|
from brindle.providers import brindle_invocation
|
|
2132
2147
|
|
|
2148
|
+
_review_worktree_present(ws)
|
|
2133
2149
|
cfg = cfg or load_repo_config(ws.repo_root)
|
|
2134
2150
|
if not cfg.checks:
|
|
2135
2151
|
return request_review(db, caller, ws, profile, focus, cfg)
|
|
@@ -2208,12 +2224,15 @@ def request_review(db: DB, caller: Agent | None, ws: Workspace, profile: str | N
|
|
|
2208
2224
|
from brindle import gates
|
|
2209
2225
|
from brindle.config import load_repo_config
|
|
2210
2226
|
|
|
2227
|
+
_review_worktree_present(ws)
|
|
2211
2228
|
_review_kill_switch(ws)
|
|
2212
2229
|
cfg = cfg or load_repo_config(ws.repo_root)
|
|
2213
2230
|
sha = gates.head(ws)
|
|
2214
2231
|
for old in db.list_agents(ws.id):
|
|
2215
2232
|
if old.mode != "review" or old.result is not None or not is_alive(old):
|
|
2216
2233
|
continue
|
|
2234
|
+
if old.unreported_noted:
|
|
2235
|
+
continue # it stopped without a verdict: not the reviewer of anything
|
|
2217
2236
|
if old.review_sha == sha:
|
|
2218
2237
|
if profile and old.profile != profile:
|
|
2219
2238
|
continue # another profile's review of this commit (a second review): keep both
|
|
@@ -2397,6 +2416,9 @@ def pre_tool_decision(db: DB, agent: Agent, payload: dict) -> dict | None:
|
|
|
2397
2416
|
return None
|
|
2398
2417
|
command = str((payload.get("tool_input") or {}).get("command", ""))
|
|
2399
2418
|
ws = db.get_workspace(agent.workspace_id)
|
|
2419
|
+
from brindle import codemap
|
|
2420
|
+
|
|
2421
|
+
codemap.record_query(db, agent, ws, command)
|
|
2400
2422
|
try:
|
|
2401
2423
|
profile = load_profile(agent.profile, ws.repo_root if ws else None)
|
|
2402
2424
|
except Exception:
|
|
@@ -2450,13 +2472,49 @@ def permission_request_decision(db: DB, agent: Agent, payload: dict,
|
|
|
2450
2472
|
if not reqs:
|
|
2451
2473
|
return None
|
|
2452
2474
|
rules = [*permissions.all_rules(ws.repo_root), *profile_rules]
|
|
2475
|
+
from dataclasses import replace
|
|
2476
|
+
|
|
2477
|
+
from brindle.native.permissions import Permissions, split_commands, uncovered_part
|
|
2453
2478
|
from brindle.profiles import load_profile
|
|
2454
2479
|
|
|
2455
2480
|
try:
|
|
2456
|
-
|
|
2481
|
+
profile = load_profile(agent.profile, ws.repo_root)
|
|
2457
2482
|
except KeyError:
|
|
2458
|
-
|
|
2483
|
+
profile = None # deleted profile: no extra allowances
|
|
2484
|
+
if profile is not None:
|
|
2485
|
+
rules += permissions.profile_bash_allows(profile)
|
|
2459
2486
|
decision = permissions.decide_all(reqs, checks=cfg.checks, rules=rules)
|
|
2487
|
+
if decision.decision == "ask" and len(reqs) == 1:
|
|
2488
|
+
# A deny on one part of a compound command (cd x && <denied>)
|
|
2489
|
+
# wins before any part-by-part allow below.
|
|
2490
|
+
decision = permissions.deny_in_parts(reqs[0], checks=cfg.checks, rules=rules) or decision
|
|
2491
|
+
if decision.decision == "ask" and profile is not None:
|
|
2492
|
+
perms = Permissions(profile.permission_mode, profile.allowed_tools)
|
|
2493
|
+
command = reqs[0].command or ""
|
|
2494
|
+
specs = [spec for name, spec in perms.rules
|
|
2495
|
+
if name == "Bash" and spec not in (None, "", "*")]
|
|
2496
|
+
cwd = os.path.realpath(reqs[0].cwd or ws.path)
|
|
2497
|
+
root = os.path.realpath(ws.path)
|
|
2498
|
+
# Like pre_tool_decision: a compound command whose every part
|
|
2499
|
+
# allowed_tools covers (a cd within the worktree counts).
|
|
2500
|
+
if (len(reqs) == 1 and reqs[0].kind == "bash" and specs
|
|
2501
|
+
and (cwd == root or cwd.startswith(root + os.sep))
|
|
2502
|
+
and uncovered_part(specs, command, cd_root=ws.path) is None):
|
|
2503
|
+
decision = permissions.Decision(
|
|
2504
|
+
"allow", "every part of the command matches the profile's allowed_tools")
|
|
2505
|
+
# Deny each part as the allow just read it (quotes and a
|
|
2506
|
+
# harmless redirection gone), which deny_in_parts doesn't.
|
|
2507
|
+
for part in split_commands(command):
|
|
2508
|
+
denied = permissions.decide(replace(reqs[0], command=part), checks=cfg.checks, rules=rules)
|
|
2509
|
+
if denied.decision == "deny":
|
|
2510
|
+
decision = denied
|
|
2511
|
+
break
|
|
2512
|
+
elif perms.mode == "dontAsk":
|
|
2513
|
+
# Nobody watches this pane: a prompt would stall the agent.
|
|
2514
|
+
decision = permissions.Decision(
|
|
2515
|
+
"deny", f"{decision.reason.removesuffix('; asking the user')}, and this profile "
|
|
2516
|
+
"never asks (permission_mode dontAsk). Don't retry it: do without it, or ask "
|
|
2517
|
+
"your supervisor with the send_message tool (brindle MCP server)")
|
|
2460
2518
|
summary = reqs[0].summary() if len(reqs) == 1 else (
|
|
2461
2519
|
f"{reqs[0].tool}: " + ", ".join(r.path or "?" for r in reqs))[:200]
|
|
2462
2520
|
_record_permission(db, agent, ws, summary, f"{decision.decision}: {decision.reason}")
|
|
@@ -2515,24 +2573,26 @@ def _stop_decision(db: DB, agent: Agent, payload: dict, via_inbox: bool = False)
|
|
|
2515
2573
|
"""What the Stop hook decides: a "block" with the reason to carry on, or
|
|
2516
2574
|
None to let the agent stop. ``via_inbox``: the reason will be sent as an
|
|
2517
2575
|
inbox message rather than block the stop, so ``stop_hook_active`` never
|
|
2518
|
-
caps a repeat; the unread-mail reminder is then limited
|
|
2576
|
+
caps a repeat; the unread-mail reminder is then limited to one per
|
|
2577
|
+
NOTICE_TTL (DB.claim_stop_reminder)."""
|
|
2519
2578
|
msg = db.pop_pending(agent.id)
|
|
2520
2579
|
if msg:
|
|
2521
2580
|
db.set_status(agent.id, "processing")
|
|
2522
2581
|
return {"decision": "block", "reason": msg.body}
|
|
2523
|
-
if agent.mode == "interactive" and db.unread_count(agent.id)
|
|
2582
|
+
if (agent.mode == "interactive" and db.unread_count(agent.id)
|
|
2583
|
+
and not payload.get("stop_hook_active")):
|
|
2584
|
+
# Stopping with mail unread, even moments after its notice went out:
|
|
2585
|
+
# that notice may have been lost, and nothing else will wake it.
|
|
2524
2586
|
ap = db.get_autopilot(agent.id)
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2587
|
+
pilot = ap is not None and ap.enabled
|
|
2588
|
+
# An autopilot supervisor that would really be blocked doesn't stop
|
|
2589
|
+
# with mail it hasn't read; stop_hook_active caps that.
|
|
2590
|
+
claimed = db.unread(agent.id) if pilot and not via_inbox else db.claim_stop_reminder(agent.id)
|
|
2591
|
+
if claimed:
|
|
2528
2592
|
db.set_status(agent.id, "processing")
|
|
2529
2593
|
return {"decision": "block", "reason": (
|
|
2530
2594
|
"[brindle autopilot] You have unread messages. Call read_messages "
|
|
2531
|
-
"before you stop.")}
|
|
2532
|
-
claimed = None if payload.get("stop_hook_active") else db.claim_notice(agent.id)
|
|
2533
|
-
if claimed: # the notice never went out (or was lost): hand it over now
|
|
2534
|
-
db.set_status(agent.id, "processing")
|
|
2535
|
-
return {"decision": "block", "reason": unread_notice(db, claimed)}
|
|
2595
|
+
"before you stop." if pilot else unread_notice(db, claimed))}
|
|
2536
2596
|
needs_report = agent.mode in REPORTING_MODES and agent.result is None
|
|
2537
2597
|
if needs_report and not payload.get("stop_hook_active"):
|
|
2538
2598
|
db.set_status(agent.id, "processing")
|
|
@@ -2590,6 +2650,11 @@ def handle_hook(db: DB, agent_id: str, event: str, payload: dict) -> dict | None
|
|
|
2590
2650
|
if transcript and agent.provider == "claude" and transcript != agent.transcript_path:
|
|
2591
2651
|
db.update_agent(agent_id, transcript_path=str(transcript))
|
|
2592
2652
|
|
|
2653
|
+
if event in ("prompt-submit", "pre-tool", "tool-done") and agent.unreported_noted:
|
|
2654
|
+
# It's working again, so a later stop without a result is a new one
|
|
2655
|
+
# its supervisor should hear about (see tell_parent_unreported).
|
|
2656
|
+
db.update_agent(agent_id, unreported_noted=None)
|
|
2657
|
+
|
|
2593
2658
|
if event == "session-start":
|
|
2594
2659
|
from brindle import inbox
|
|
2595
2660
|
|
|
@@ -2694,6 +2759,13 @@ def handle_hook(db: DB, agent_id: str, event: str, payload: dict) -> dict | None
|
|
|
2694
2759
|
decision = pilot.on_stop(db, agent, payload)
|
|
2695
2760
|
if decision:
|
|
2696
2761
|
db.enqueue(agent_id, decision["reason"], None)
|
|
2762
|
+
if (agent.mode in REPORTING_MODES and agent.result is None
|
|
2763
|
+
and not db.pending_count(agent_id)):
|
|
2764
|
+
# No Stop hook can remind it to report, and no hook says when
|
|
2765
|
+
# it resumes: every turn it ends without a result is a stop
|
|
2766
|
+
# its supervisor hears about.
|
|
2767
|
+
db.update_agent(agent_id, unreported_noted=None)
|
|
2768
|
+
tell_parent_unreported(db, db.get_agent(agent_id) or agent)
|
|
2697
2769
|
if db.pending_count(agent_id) and not agent.headless:
|
|
2698
2770
|
flush(db, agent_id)
|
|
2699
2771
|
elif event == "stop-failure":
|
|
@@ -2721,17 +2793,32 @@ def handle_hook(db: DB, agent_id: str, event: str, payload: dict) -> dict | None
|
|
|
2721
2793
|
"oauth_org_not_allowed", "billing_error"
|
|
2722
2794
|
))
|
|
2723
2795
|
)
|
|
2724
|
-
|
|
2796
|
+
# Claude Code's line for a --model its backend doesn't serve: every
|
|
2797
|
+
# turn fails the same way, so the agent can't go on (launch_health
|
|
2798
|
+
# reads the same line on screen).
|
|
2799
|
+
from brindle import launch_health
|
|
2800
|
+
|
|
2801
|
+
is_model = not is_rate_limit and not is_auth and bool(
|
|
2802
|
+
re.search(launch_health.SELECTED_MODEL, err_fields))
|
|
2803
|
+
if is_auth or is_model:
|
|
2725
2804
|
db.set_status(agent_id, "failed")
|
|
2726
2805
|
db.clear_permission_requests(agent_id)
|
|
2727
2806
|
from brindle import providers, autopilot as pilot
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2807
|
+
if is_auth:
|
|
2808
|
+
providers.invalidate_signed_in(agent.provider)
|
|
2809
|
+
err_desc = payload.get("error_details") or payload.get("error") or payload.get("error_type") or "401/403"
|
|
2810
|
+
msg = (
|
|
2811
|
+
f"[brindle] Agent {agent_id} encountered an authentication failure "
|
|
2812
|
+
f"(invalid or expired credentials): {err_desc}. "
|
|
2813
|
+
"Check API keys or login status."
|
|
2814
|
+
)
|
|
2815
|
+
else:
|
|
2816
|
+
err_desc = "its model isn't available on this route"
|
|
2817
|
+
msg = (
|
|
2818
|
+
f"[brindle] Agent {agent_id} failed: Claude Code says there's an issue with "
|
|
2819
|
+
"the selected model (it may not exist, or the account or route may lack access). "
|
|
2820
|
+
"Check the profile's model and the Claude route (`brindle doctor`), then retry."
|
|
2821
|
+
)
|
|
2735
2822
|
if agent.parent_id:
|
|
2736
2823
|
try:
|
|
2737
2824
|
send_message(db, agent.parent_id, msg, sender_id="brindle")
|
|
@@ -2742,7 +2829,8 @@ def handle_hook(db: DB, agent_id: str, event: str, payload: dict) -> dict | None
|
|
|
2742
2829
|
pass
|
|
2743
2830
|
root_id = pilot.root_of(db, agent_id)
|
|
2744
2831
|
if db.get_autopilot(root_id):
|
|
2745
|
-
|
|
2832
|
+
what = "Credentials need" if is_auth else "The model needs"
|
|
2833
|
+
pilot.need_user(db, root_id, f"{what} attention for agent {agent_id}: {err_desc}")
|
|
2746
2834
|
else:
|
|
2747
2835
|
db.set_status(agent_id, "idle")
|
|
2748
2836
|
db.clear_permission_requests(agent_id)
|
|
@@ -2817,30 +2905,36 @@ def command_running(db: DB, agent: Agent) -> bool:
|
|
|
2817
2905
|
return False
|
|
2818
2906
|
|
|
2819
2907
|
|
|
2820
|
-
def tell_parent_unreported(db: DB, agent: Agent) ->
|
|
2908
|
+
def tell_parent_unreported(db: DB, agent: Agent) -> bool:
|
|
2821
2909
|
"""A worker stopped again after being reminded to report, still without a
|
|
2822
2910
|
result. It won't be reminded again on its own, and its supervisor has
|
|
2823
2911
|
usually gone idle waiting for it with nothing left to wake it, so tell the
|
|
2824
|
-
supervisor (queued if it's busy, delivered now if it's idle).
|
|
2912
|
+
supervisor (queued if it's busy, delivered now if it's idle). Returns
|
|
2913
|
+
whether the supervisor was told."""
|
|
2825
2914
|
if agent.mode not in FORWARDING_MODES or not agent.parent_id:
|
|
2826
|
-
return # a synchronous handoff's caller is still waiting on it
|
|
2915
|
+
return False # a synchronous handoff's caller is still waiting on it
|
|
2827
2916
|
if db.get_agent(agent.parent_id) is None:
|
|
2828
|
-
return
|
|
2917
|
+
return False
|
|
2829
2918
|
ws = db.get_workspace(agent.workspace_id)
|
|
2830
|
-
if agent.mode == "review" and ws is not None:
|
|
2919
|
+
if agent.mode == "review" and ws is not None and os.path.isdir(ws.path):
|
|
2831
2920
|
from brindle import gates
|
|
2832
2921
|
|
|
2833
2922
|
if db.latest_review(ws.id, gates.head(ws)):
|
|
2834
|
-
return # its verdict for the current commit is already recorded
|
|
2923
|
+
return False # its verdict for the current commit is already recorded
|
|
2835
2924
|
if agent.unreported_noted:
|
|
2836
|
-
return # the supervisor was told
|
|
2925
|
+
return False # the supervisor was told about this stop; cleared once it resumes
|
|
2837
2926
|
if command_running(db, agent):
|
|
2838
2927
|
# Its turn ended while a long or background command (a test run) is
|
|
2839
2928
|
# still going: it isn't stopped, and the command finishing wakes it.
|
|
2840
2929
|
# Not marked noted, so a real stop later still gets its one alert.
|
|
2841
|
-
return
|
|
2930
|
+
return False
|
|
2842
2931
|
db.update_agent(agent.id, unreported_noted=1)
|
|
2843
|
-
|
|
2932
|
+
if agent.mode == "review" and ws is not None:
|
|
2933
|
+
from brindle import pipeline
|
|
2934
|
+
|
|
2935
|
+
if pipeline.on_reviewer_unreported(db, get(db, agent.id), ws):
|
|
2936
|
+
return True # a fallback reviewer took over, or the supervisor was told
|
|
2937
|
+
where =f" (branch `{ws.branch}`, workspace {ws.id})" if ws else ""
|
|
2844
2938
|
tool = "submit_review" if agent.mode == "review" else "report_result"
|
|
2845
2939
|
who = "Reviewer" if agent.mode == "review" else "Worker"
|
|
2846
2940
|
body = (
|
|
@@ -2853,7 +2947,8 @@ def tell_parent_unreported(db: DB, agent: Agent) -> None:
|
|
|
2853
2947
|
except (AgentError, tmux.TmuxError):
|
|
2854
2948
|
# The parent isn't running (or can't take messages): the notice is
|
|
2855
2949
|
# deliberately dropped here, since there's nothing left to tell.
|
|
2856
|
-
|
|
2950
|
+
return False
|
|
2951
|
+
return True
|
|
2857
2952
|
|
|
2858
2953
|
|
|
2859
2954
|
PERMISSION_EVENTS = ("permission-request", "codex-permission-request")
|