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.
Files changed (134) hide show
  1. {brindle-0.3.11 → brindle-0.3.13}/PKG-INFO +56 -4
  2. {brindle-0.3.11 → brindle-0.3.13}/README.md +55 -3
  3. {brindle-0.3.11 → brindle-0.3.13}/pyproject.toml +1 -1
  4. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/agents.py +142 -47
  5. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/antigravity.py +54 -8
  6. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/autopilot.py +3 -0
  7. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/cli.py +4 -1
  8. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/cloud_credential.py +24 -4
  9. brindle-0.3.13/src/brindle/codemap.py +422 -0
  10. brindle-0.3.13/src/brindle/codemap_docs.py +65 -0
  11. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/company_login.py +35 -8
  12. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/cost.py +17 -2
  13. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/cull.py +64 -6
  14. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/db.py +24 -2
  15. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/doctor.py +29 -0
  16. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/events.py +29 -6
  17. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/history.py +1 -1
  18. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/launch_health.py +8 -3
  19. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/learning.py +14 -10
  20. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/native/permissions.py +113 -14
  21. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/permissions.py +70 -12
  22. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pipeline.py +60 -6
  23. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/learning.py +11 -9
  24. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/managed_models.py +22 -4
  25. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/route_health.py +5 -0
  26. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/settings_sync.py +5 -4
  27. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/team_policy.py +26 -3
  28. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/providers.py +24 -2
  29. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/usage.py +44 -7
  30. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/workspaces.py +20 -7
  31. brindle-0.3.11/src/brindle/codemap.py +0 -73
  32. {brindle-0.3.11 → brindle-0.3.13}/.gitignore +0 -0
  33. {brindle-0.3.11 → brindle-0.3.13}/LICENSE +0 -0
  34. {brindle-0.3.11 → brindle-0.3.13}/SCHEDULE-A +0 -0
  35. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/__init__.py +0 -0
  36. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/__main__.py +0 -0
  37. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/account.py +0 -0
  38. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/airgap.py +0 -0
  39. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/budget.py +0 -0
  40. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/__init__.py +0 -0
  41. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/backend-auditor.md +0 -0
  42. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/developer-codex.md +0 -0
  43. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/developer-fable.md +0 -0
  44. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/developer-gemini.md +0 -0
  45. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/developer-heavy.md +0 -0
  46. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/developer-light.md +0 -0
  47. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/developer-local.md +0 -0
  48. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/developer.md +0 -0
  49. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/qa.md +0 -0
  50. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/reviewer-codex.md +0 -0
  51. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/reviewer-gemini.md +0 -0
  52. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/reviewer-local.md +0 -0
  53. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/reviewer.md +0 -0
  54. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/subagent.md +0 -0
  55. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_agents/supervisor.md +0 -0
  56. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_rules/__init__.py +0 -0
  57. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_rules/security/backend.md +0 -0
  58. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_rules/style/minimal-diff.md +0 -0
  59. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/builtin_rules/tests/only.md +0 -0
  60. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/ci_adapters.py +0 -0
  61. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/ci_client.py +0 -0
  62. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/ci_federation.py +0 -0
  63. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/ci_hosts.py +0 -0
  64. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/codex_hook.py +0 -0
  65. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/company_identity.py +0 -0
  66. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/config.py +0 -0
  67. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/conflicts.py +0 -0
  68. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/cost_estimate.py +0 -0
  69. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/demo.py +0 -0
  70. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/detect.py +0 -0
  71. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/escapes.py +0 -0
  72. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/gates.py +0 -0
  73. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/git.py +0 -0
  74. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/guardrails.py +0 -0
  75. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/identity_lock.py +0 -0
  76. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/inbox.py +0 -0
  77. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/keys_cmds.py +0 -0
  78. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/keystore.py +0 -0
  79. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/learned_rules.py +0 -0
  80. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/legacy.py +0 -0
  81. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/mcp_server.py +0 -0
  82. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/model_access.py +0 -0
  83. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/model_availability.py +0 -0
  84. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/native/__init__.py +0 -0
  85. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/native/client.py +0 -0
  86. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/native/loop.py +0 -0
  87. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/native/runner.py +0 -0
  88. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/native/serve.py +0 -0
  89. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/native/tools.py +0 -0
  90. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pane_auth.py +0 -0
  91. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/plugins.py +0 -0
  92. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/policy.py +0 -0
  93. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pool.py +0 -0
  94. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pricing.py +0 -0
  95. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/__init__.py +0 -0
  96. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/_files.py +0 -0
  97. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/account.py +0 -0
  98. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/audit_chain.py +0 -0
  99. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/audit_export.py +0 -0
  100. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/auth.py +0 -0
  101. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/brief_learning.py +0 -0
  102. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/cost_centers.py +0 -0
  103. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/credentials.py +0 -0
  104. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/features.py +0 -0
  105. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/keys.py +0 -0
  106. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/license.py +0 -0
  107. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/loopback.py +0 -0
  108. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/org_profiles.py +0 -0
  109. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/orgkey.py +0 -0
  110. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/rollout.py +0 -0
  111. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/status.py +0 -0
  112. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/pro/team_events.py +0 -0
  113. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/procs.py +0 -0
  114. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/profiles.py +0 -0
  115. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/quota.py +0 -0
  116. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/repo_cmds.py +0 -0
  117. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/repos.py +0 -0
  118. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/rewind.py +0 -0
  119. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/rule_checks.py +0 -0
  120. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/savings.py +0 -0
  121. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/scratch.py +0 -0
  122. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/secrets.py +0 -0
  123. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/self_serve.py +0 -0
  124. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/services.py +0 -0
  125. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/sessions.py +0 -0
  126. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/sidebar_follow.py +0 -0
  127. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/signin_pause.py +0 -0
  128. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/status_cache.py +0 -0
  129. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/tasks.py +0 -0
  130. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/tmux.py +0 -0
  131. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/updates.py +0 -0
  132. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/view.py +0 -0
  133. {brindle-0.3.11 → brindle-0.3.13}/src/brindle/watch.py +0 -0
  134. {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.11
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.11`). A session started before an upgrade keeps
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 (keep the key out of
1912
- the repo). The key alone does nothing: without `modelProvider`, `agy` still asks
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.11`). A session started before an upgrade keeps
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 (keep the key out of
1881
- the repo). The key alone does nothing: without `modelProvider`, `agy` still asks
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.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "brindle"
3
- version = "0.3.11"
3
+ version = "0.3.13"
4
4
  description = "Run coding agents in parallel, each on its own git branch, with a supervisor that delegates, reviews, and merges"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -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.repo_root), test_guidance(checks),
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.repo_root)
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
- # Workers receive their instructions and real task in one delivery.
815
- # A fresh interactive chat without a task still needs its profile.
816
- if provider.name == "antigravity" and not resume and (prompt or agent.mode == "interactive"):
817
- from brindle import antigravity
818
- instructions = "\n\n".join(filter(None, (antigravity.TOOLS_NOTE, profile.prompt)))
819
- prompt = "\n\n".join(filter(None, (antigravity.format_delivered_message(instructions), prompt)))
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
- rules += permissions.profile_bash_allows(load_profile(agent.profile, ws.repo_root))
2481
+ profile = load_profile(agent.profile, ws.repo_root)
2457
2482
  except KeyError:
2458
- pass # deleted profile: no extra allowances
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 like a notice."""
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
- if (ap is not None and ap.enabled and not payload.get("stop_hook_active")
2526
- and (not via_inbox or db.claim_notice(agent.id))):
2527
- # An autopilot supervisor doesn't stop with mail it hasn't read.
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
- if is_auth:
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
- providers.invalidate_signed_in(agent.provider)
2729
- err_desc = payload.get("error_details") or payload.get("error") or payload.get("error_type") or "401/403"
2730
- msg = (
2731
- f"[brindle] Agent {agent_id} encountered an authentication failure "
2732
- f"(invalid or expired credentials): {err_desc}. "
2733
- "Check API keys or login status."
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
- pilot.need_user(db, root_id, f"Credentials need attention for agent {agent_id}: {err_desc}")
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) -> None:
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 once; no repeats
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
- where = f" (branch `{ws.branch}`, workspace {ws.id})" if ws else ""
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
- pass
2950
+ return False
2951
+ return True
2857
2952
 
2858
2953
 
2859
2954
  PERMISSION_EVENTS = ("permission-request", "codex-permission-request")