brindle 0.3.16__tar.gz → 0.3.18__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. {brindle-0.3.16 → brindle-0.3.18}/PKG-INFO +9 -6
  2. {brindle-0.3.16 → brindle-0.3.18}/README.md +8 -5
  3. {brindle-0.3.16 → brindle-0.3.18}/pyproject.toml +1 -1
  4. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/agents.py +133 -19
  5. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/autopilot.py +406 -102
  6. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/cli.py +29 -5
  7. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/company_identity.py +9 -3
  8. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/company_login.py +13 -1
  9. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/cull.py +133 -5
  10. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/db.py +36 -5
  11. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/git.py +40 -0
  12. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/history.py +96 -4
  13. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/launch_health.py +77 -4
  14. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/mcp_server.py +31 -14
  15. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pipeline.py +2 -1
  16. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/account.py +6 -0
  17. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/managed_models.py +6 -2
  18. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/team_policy.py +16 -2
  19. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/prompt_answer.py +25 -1
  20. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/providers.py +41 -0
  21. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/sessions.py +13 -3
  22. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/sidebar_follow.py +3 -0
  23. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/usage.py +21 -2
  24. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/view.py +37 -14
  25. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/watch.py +26 -11
  26. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/watch_costs.py +33 -9
  27. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/workspaces.py +15 -4
  28. {brindle-0.3.16 → brindle-0.3.18}/.gitignore +0 -0
  29. {brindle-0.3.16 → brindle-0.3.18}/LICENSE +0 -0
  30. {brindle-0.3.16 → brindle-0.3.18}/SCHEDULE-A +0 -0
  31. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/__init__.py +0 -0
  32. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/__main__.py +0 -0
  33. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/account.py +0 -0
  34. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/airgap.py +0 -0
  35. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/antigravity.py +0 -0
  36. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/budget.py +0 -0
  37. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/__init__.py +0 -0
  38. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/backend-auditor.md +0 -0
  39. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/developer-codex.md +0 -0
  40. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/developer-fable.md +0 -0
  41. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/developer-gemini.md +0 -0
  42. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/developer-heavy.md +0 -0
  43. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/developer-light.md +0 -0
  44. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/developer-local.md +0 -0
  45. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/developer.md +0 -0
  46. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/qa.md +0 -0
  47. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/reviewer-codex.md +0 -0
  48. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/reviewer-gemini.md +0 -0
  49. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/reviewer-local.md +0 -0
  50. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/reviewer.md +0 -0
  51. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/subagent.md +0 -0
  52. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_agents/supervisor.md +0 -0
  53. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_rules/__init__.py +0 -0
  54. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_rules/security/backend.md +0 -0
  55. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_rules/style/minimal-diff.md +0 -0
  56. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/builtin_rules/tests/only.md +0 -0
  57. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/ci_adapters.py +0 -0
  58. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/ci_client.py +0 -0
  59. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/ci_federation.py +0 -0
  60. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/ci_hosts.py +0 -0
  61. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/cloud_credential.py +0 -0
  62. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/cloud_login.py +0 -0
  63. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/codemap.py +0 -0
  64. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/codemap_docs.py +0 -0
  65. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/codex_hook.py +0 -0
  66. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/config.py +0 -0
  67. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/conflicts.py +0 -0
  68. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/cost.py +0 -0
  69. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/cost_estimate.py +0 -0
  70. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/demo.py +0 -0
  71. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/detect.py +0 -0
  72. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/doctor.py +0 -0
  73. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/escapes.py +0 -0
  74. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/events.py +0 -0
  75. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/gates.py +0 -0
  76. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/guardrails.py +0 -0
  77. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/identity_lock.py +0 -0
  78. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/inbox.py +0 -0
  79. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/keys_cmds.py +0 -0
  80. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/keystore.py +0 -0
  81. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/learned_rules.py +0 -0
  82. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/learning.py +0 -0
  83. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/legacy.py +0 -0
  84. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/login_pause.py +0 -0
  85. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/model_access.py +0 -0
  86. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/model_availability.py +0 -0
  87. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/native/__init__.py +0 -0
  88. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/native/client.py +0 -0
  89. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/native/loop.py +0 -0
  90. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/native/permissions.py +0 -0
  91. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/native/runner.py +0 -0
  92. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/native/serve.py +0 -0
  93. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/native/tools.py +0 -0
  94. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pane_auth.py +0 -0
  95. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/permissions.py +0 -0
  96. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/plugins.py +0 -0
  97. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/policy.py +0 -0
  98. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pool.py +0 -0
  99. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pricing.py +0 -0
  100. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/__init__.py +0 -0
  101. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/_files.py +0 -0
  102. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/audit_chain.py +0 -0
  103. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/audit_export.py +0 -0
  104. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/auth.py +0 -0
  105. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/brief_learning.py +0 -0
  106. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/cost_centers.py +0 -0
  107. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/credentials.py +0 -0
  108. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/features.py +0 -0
  109. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/keys.py +0 -0
  110. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/learning.py +0 -0
  111. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/license.py +0 -0
  112. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/loopback.py +0 -0
  113. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/org_profiles.py +0 -0
  114. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/orgkey.py +0 -0
  115. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/rollout.py +0 -0
  116. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/route_health.py +0 -0
  117. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/settings_sync.py +0 -0
  118. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/status.py +0 -0
  119. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/pro/team_events.py +0 -0
  120. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/procs.py +0 -0
  121. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/profiles.py +0 -0
  122. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/quota.py +0 -0
  123. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/repo_cmds.py +0 -0
  124. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/repos.py +0 -0
  125. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/rewind.py +0 -0
  126. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/rule_checks.py +0 -0
  127. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/savings.py +0 -0
  128. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/scratch.py +0 -0
  129. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/secrets.py +0 -0
  130. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/self_serve.py +0 -0
  131. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/services.py +0 -0
  132. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/signin_pause.py +0 -0
  133. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/status_cache.py +0 -0
  134. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/tasks.py +0 -0
  135. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/tmux.py +0 -0
  136. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/updates.py +0 -0
  137. {brindle-0.3.16 → brindle-0.3.18}/src/brindle/upgrade.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: brindle
3
- Version: 0.3.16
3
+ Version: 0.3.18
4
4
  Summary: Run coding agents in parallel, each on its own git branch, with a supervisor that delegates, reviews, and merges
5
5
  Project-URL: Homepage, https://pawdelta.com/brindle/
6
6
  Project-URL: Documentation, https://pawdelta.com/brindle/docs/recommended-use
@@ -94,7 +94,7 @@ uv tool install --editable ~/Projects/brindle # or from a local checkout
94
94
  brindle drives Claude Code, so you need that too (`npm install -g @anthropic-ai/claude-code`).
95
95
 
96
96
  `brindle --version` prints the installed version; the tmux status bar of every brindle
97
- session shows it too (`brindle 0.3.16`). A session started before an upgrade keeps
97
+ session shows it too (`brindle 0.3.18`). A session started before an upgrade keeps
98
98
  running the old code, and shows the old number, until you restart it.
99
99
 
100
100
  ## Quick start
@@ -415,7 +415,7 @@ knowing them helps when you tell the supervisor how to work.
415
415
  | `complete_subagent` | supervisor | record the result of a `subagent`-profile task |
416
416
  | `set_goal` / `get_progress` / `check_milestone` | supervisor | autopilot's goal, its progress, and running the checks |
417
417
  | `record_audit_decision` | supervisor | record acceptance of an audit finding with rationale so subsequent audits respect it |
418
- | `need_user` | supervisor | stop autopilot and ask you a question |
418
+ | `need_user` | supervisor | ask you a question (a decision, a denied action, a stalled external job such as a CI run no runner picks up); only the steps that depend on it wait, the rest keeps going; `answered=true` once you've replied |
419
419
  | `transfer_to_repo` | supervisor | move a scratch session's work into a repository |
420
420
  | `handover` | supervisor | hand the session to a new supervisor on another branch or worktree, with a note |
421
421
 
@@ -641,6 +641,8 @@ wins. When every candidate is out, the repo's `default_agent` runs. The reply sa
641
641
  what was picked and why, e.g. `weight medium -> developer (Codex at 93%, skipped developer-codex)`.
642
642
  An `agent_profile` you pass, or a milestone's `profile`, always wins over weight.
643
643
 
644
+ The supervisor runs on your Claude Code model. If your organisation's Claude Code managed settings pin the model (e.g. to Sonnet), that applies to the brindle supervisor too. Supervising goes best on Opus (or Fable where available), so if your org allows it, switch in the supervisor's own session with `/model`. Worker profiles set their own model and aren't affected.
645
+
644
646
  Provisioning and routing also check explicit models against supported read-only
645
647
  catalogs, with the managed model, stored keys and the worker's filtered pane
646
648
  environment. A model absent from a complete authoritative catalog is rejected
@@ -1419,7 +1421,7 @@ change files under a directory without prompts.
1419
1421
 
1420
1422
  ### Which provider can do what
1421
1423
 
1422
- | Provider | Supervisor (`brindle --provider …`) | Worker / reviewer |
1424
+ | Provider | Supervisor (`brindle --supervisor …`) | Worker / reviewer |
1423
1425
  |---|---|---|
1424
1426
  | `claude` (Claude Code) | yes (the default) | yes |
1425
1427
  | `codex` | yes | yes |
@@ -1481,7 +1483,7 @@ When running long, unattended autopilot sessions with API keys:
1481
1483
  - **Codex (unsupported runtime auth detection)**: Codex CLI turn completion (`agent-turn-complete`) does not classify auth errors at runtime. If an OpenAI API key expires or is rejected, Codex workers will not automatically transition to `failed` or trigger an autopilot credential pause. When running Codex workers unattended, monitor their progress via `brindle watch` or check tmux panes; if an auth failure occurs, manually restart or rotate keys.
1482
1484
  - **Supervisor credential refresh and key rotation**:
1483
1485
  - **Recovering failed/blocked workers**: When an agent fails on an expired key or autopilot pauses with a credential error, export the fresh key (or update `brindle keys set`), then run `brindle continue`. This unpauses the session and relaunches paused/failed agents with the new credentials. The replacement credentials are automatically synced to the session credential store, dynamically refreshed by the supervisor's launcher on all subsequent worker delegations (`assign`/`handoff`).
1484
- - **Proactive rotation in live sessions**: Exporting a new key while an active session is healthy does not restart running workers or the supervisor (running `brindle continue` on an active healthy session simply reattaches to tmux without restarting workers). To proactively rotate credentials for the entire session and all running workers: run `brindle stop` -> update credentials in your environment or via `brindle keys set` -> run `brindle continue`.
1486
+ - **Proactive rotation in live sessions**: For Claude keys rotated outside brindle, set Claude Code's `apiKeyHelper` in `~/.claude/settings.json` or the project's `.claude/settings.json`. Claude Code re-runs the helper every 5 minutes (configurable with `CLAUDE_CODE_API_KEY_HELPER_TTL_MS`) and after a 401, so running workers pick up the new key. Don't also export `ANTHROPIC_API_KEY` or set a Claude key with `brindle keys set`, since an environment key takes precedence over the helper. Codex and Antigravity keys still need workers stopped, then `brindle keys set`, then `brindle continue`. Exporting a new key while an active session is healthy does not restart running workers or the supervisor (running `brindle continue` on an active healthy session simply reattaches to tmux without restarting workers). To proactively rotate credentials for the entire session and all running workers: run `brindle stop` -> update credentials in your environment or via `brindle keys set` -> run `brindle continue`.
1485
1487
  - **Precedence**: Newly stored keys (`brindle keys set`) or session credentials updated during recovery take effect dynamically for all new panes, overriding stale launcher environments. For profile-level overrides, an explicit profile key (`env.NAME`) takes precedence over default provider credentials.
1486
1488
  - **Antigravity permissions prerequisite**: Antigravity (`agy`) does not support approving shell commands solely via external hooks. With `GEMINI_API_KEY`, unattended agy workers will still stall on approval prompts unless permissions are pre-populated in `settings.json` (see Permissions) or run with `agy_approvals: "brindle"` under `permission_policy: "on"` (`--dangerously-skip-permissions`).
1487
1489
 
@@ -1966,7 +1968,8 @@ you to sign in. `agy` takes no other key or token from the environment; its
1966
1968
  Google Cloud, Workforce Identity and Application Default Credentials sign-ins are
1967
1969
  chosen on its sign-in screen.
1968
1970
 
1969
- Then run the whole session on it with `brindle --provider antigravity`, or mix models:
1971
+ Then run the supervisor on it with `brindle --supervisor antigravity`.
1972
+ Workers keep their own profiles' providers. To mix models,
1970
1973
  give a profile `provider: antigravity` (for example a `gemini-reviewer` for a second
1971
1974
  model's review) and the supervisor can hand it tasks.
1972
1975
 
@@ -63,7 +63,7 @@ uv tool install --editable ~/Projects/brindle # or from a local checkout
63
63
  brindle drives Claude Code, so you need that too (`npm install -g @anthropic-ai/claude-code`).
64
64
 
65
65
  `brindle --version` prints the installed version; the tmux status bar of every brindle
66
- session shows it too (`brindle 0.3.16`). A session started before an upgrade keeps
66
+ session shows it too (`brindle 0.3.18`). A session started before an upgrade keeps
67
67
  running the old code, and shows the old number, until you restart it.
68
68
 
69
69
  ## Quick start
@@ -384,7 +384,7 @@ knowing them helps when you tell the supervisor how to work.
384
384
  | `complete_subagent` | supervisor | record the result of a `subagent`-profile task |
385
385
  | `set_goal` / `get_progress` / `check_milestone` | supervisor | autopilot's goal, its progress, and running the checks |
386
386
  | `record_audit_decision` | supervisor | record acceptance of an audit finding with rationale so subsequent audits respect it |
387
- | `need_user` | supervisor | stop autopilot and ask you a question |
387
+ | `need_user` | supervisor | ask you a question (a decision, a denied action, a stalled external job such as a CI run no runner picks up); only the steps that depend on it wait, the rest keeps going; `answered=true` once you've replied |
388
388
  | `transfer_to_repo` | supervisor | move a scratch session's work into a repository |
389
389
  | `handover` | supervisor | hand the session to a new supervisor on another branch or worktree, with a note |
390
390
 
@@ -610,6 +610,8 @@ wins. When every candidate is out, the repo's `default_agent` runs. The reply sa
610
610
  what was picked and why, e.g. `weight medium -> developer (Codex at 93%, skipped developer-codex)`.
611
611
  An `agent_profile` you pass, or a milestone's `profile`, always wins over weight.
612
612
 
613
+ The supervisor runs on your Claude Code model. If your organisation's Claude Code managed settings pin the model (e.g. to Sonnet), that applies to the brindle supervisor too. Supervising goes best on Opus (or Fable where available), so if your org allows it, switch in the supervisor's own session with `/model`. Worker profiles set their own model and aren't affected.
614
+
613
615
  Provisioning and routing also check explicit models against supported read-only
614
616
  catalogs, with the managed model, stored keys and the worker's filtered pane
615
617
  environment. A model absent from a complete authoritative catalog is rejected
@@ -1388,7 +1390,7 @@ change files under a directory without prompts.
1388
1390
 
1389
1391
  ### Which provider can do what
1390
1392
 
1391
- | Provider | Supervisor (`brindle --provider …`) | Worker / reviewer |
1393
+ | Provider | Supervisor (`brindle --supervisor …`) | Worker / reviewer |
1392
1394
  |---|---|---|
1393
1395
  | `claude` (Claude Code) | yes (the default) | yes |
1394
1396
  | `codex` | yes | yes |
@@ -1450,7 +1452,7 @@ When running long, unattended autopilot sessions with API keys:
1450
1452
  - **Codex (unsupported runtime auth detection)**: Codex CLI turn completion (`agent-turn-complete`) does not classify auth errors at runtime. If an OpenAI API key expires or is rejected, Codex workers will not automatically transition to `failed` or trigger an autopilot credential pause. When running Codex workers unattended, monitor their progress via `brindle watch` or check tmux panes; if an auth failure occurs, manually restart or rotate keys.
1451
1453
  - **Supervisor credential refresh and key rotation**:
1452
1454
  - **Recovering failed/blocked workers**: When an agent fails on an expired key or autopilot pauses with a credential error, export the fresh key (or update `brindle keys set`), then run `brindle continue`. This unpauses the session and relaunches paused/failed agents with the new credentials. The replacement credentials are automatically synced to the session credential store, dynamically refreshed by the supervisor's launcher on all subsequent worker delegations (`assign`/`handoff`).
1453
- - **Proactive rotation in live sessions**: Exporting a new key while an active session is healthy does not restart running workers or the supervisor (running `brindle continue` on an active healthy session simply reattaches to tmux without restarting workers). To proactively rotate credentials for the entire session and all running workers: run `brindle stop` -> update credentials in your environment or via `brindle keys set` -> run `brindle continue`.
1455
+ - **Proactive rotation in live sessions**: For Claude keys rotated outside brindle, set Claude Code's `apiKeyHelper` in `~/.claude/settings.json` or the project's `.claude/settings.json`. Claude Code re-runs the helper every 5 minutes (configurable with `CLAUDE_CODE_API_KEY_HELPER_TTL_MS`) and after a 401, so running workers pick up the new key. Don't also export `ANTHROPIC_API_KEY` or set a Claude key with `brindle keys set`, since an environment key takes precedence over the helper. Codex and Antigravity keys still need workers stopped, then `brindle keys set`, then `brindle continue`. Exporting a new key while an active session is healthy does not restart running workers or the supervisor (running `brindle continue` on an active healthy session simply reattaches to tmux without restarting workers). To proactively rotate credentials for the entire session and all running workers: run `brindle stop` -> update credentials in your environment or via `brindle keys set` -> run `brindle continue`.
1454
1456
  - **Precedence**: Newly stored keys (`brindle keys set`) or session credentials updated during recovery take effect dynamically for all new panes, overriding stale launcher environments. For profile-level overrides, an explicit profile key (`env.NAME`) takes precedence over default provider credentials.
1455
1457
  - **Antigravity permissions prerequisite**: Antigravity (`agy`) does not support approving shell commands solely via external hooks. With `GEMINI_API_KEY`, unattended agy workers will still stall on approval prompts unless permissions are pre-populated in `settings.json` (see Permissions) or run with `agy_approvals: "brindle"` under `permission_policy: "on"` (`--dangerously-skip-permissions`).
1456
1458
 
@@ -1935,7 +1937,8 @@ you to sign in. `agy` takes no other key or token from the environment; its
1935
1937
  Google Cloud, Workforce Identity and Application Default Credentials sign-ins are
1936
1938
  chosen on its sign-in screen.
1937
1939
 
1938
- Then run the whole session on it with `brindle --provider antigravity`, or mix models:
1940
+ Then run the supervisor on it with `brindle --supervisor antigravity`.
1941
+ Workers keep their own profiles' providers. To mix models,
1939
1942
  give a profile `provider: antigravity` (for example a `gemini-reviewer` for a second
1940
1943
  model's review) and the supervisor can hand it tasks.
1941
1944
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "brindle"
3
- version = "0.3.16"
3
+ version = "0.3.18"
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"
@@ -300,9 +300,11 @@ def decorate_worker_prompt(task: str, agent_id: str, ws: Workspace, done_when: s
300
300
  prompt += PLAN_FIRST_NOTE
301
301
  prompt += WORKER_FOOTER.format(agent_id=agent_id, branch=ws.branch,
302
302
  guidance=worker_guidance(ws))
303
- if done_when and provider.name == "claude" and not headless:
304
- # /goal is an interactive command; headless workers get the finish
305
- # line above and the Stop hook's reminder to report.
303
+ if (done_when and provider.name == "claude" and not headless
304
+ and not provider.hooks_disabled(ws.path)):
305
+ # /goal is an interactive command that Claude Code refuses when hooks
306
+ # are disabled (#133), and headless workers get the finish line above
307
+ # and the Stop hook's reminder to report either way.
306
308
  prompt = pilot.worker_goal(prompt, done_when, ws.branch) or prompt
307
309
  return prompt
308
310
 
@@ -322,10 +324,12 @@ def spawn(
322
324
  autopilot: bool = False,
323
325
  plan_first: bool = False,
324
326
  review_sha: str | None = None,
327
+ claim_goals: bool = True,
325
328
  ) -> Agent:
326
329
  """Start an agent in ``ws``. Workers (handoff/assign) given a ``done_when``
327
330
  finish line run it as a Claude Code ``/goal``. With ``autopilot``, the
328
- agent is a session root that drives toward a goal (see brindle.autopilot)."""
331
+ agent is a session root that drives toward a goal (see brindle.autopilot);
332
+ with ``claim_goals`` it claims a hand-written ``.brindle/goals.md``."""
329
333
  from brindle import autopilot as pilot
330
334
  from brindle import identity_lock
331
335
 
@@ -377,9 +381,9 @@ def spawn(
377
381
  db.update_agent(agent_id, plan_first=1)
378
382
  agent.plan_first = 1
379
383
  if autopilot:
380
- plan = pilot.enable(db, agent_id, ws)
384
+ plan = pilot.enable(db, agent_id, ws, claim=claim_goals)
381
385
  if plan and not prompt:
382
- prompt = agent.task = pilot.kickoff(plan)
386
+ prompt = agent.task = pilot.kickoff(plan, agent_id)
383
387
  db.update_agent(agent_id, task=prompt)
384
388
  if mode == "interactive":
385
389
  from brindle import codemap
@@ -1177,6 +1181,10 @@ def resume(db: DB, root_id: str, *, watch_pane: bool = True,
1177
1181
  if a.status in ("paused", "failed") and (only is None or a.id in only)]
1178
1182
  for a in candidates: # all or nothing: a session never half-resumes under another identity
1179
1183
  identity_lock.ensure(a)
1184
+ if any(a.id == root_id for a in candidates):
1185
+ from brindle import autopilot as pilot
1186
+
1187
+ pilot.reload_goals_file(db, root_id) # edits the person made to this session's goal file
1180
1188
  resumed = []
1181
1189
  for a in candidates:
1182
1190
  if a.status == "failed":
@@ -1506,14 +1514,22 @@ def screen_status(db: DB, agent: Agent, samples: int = 2, gap: float = 0.7) -> s
1506
1514
  single cheap read, as the dashboard does, leaves it alone. Moving an
1507
1515
  agent to idle also delivers anything queued for it, but only when
1508
1516
  ``samples`` >= 2: a single-sample read must never pop a message into a
1509
- terminal as a side effect of just rendering the dashboard."""
1517
+ terminal as a side effect of just rendering the dashboard.
1518
+
1519
+ An agent whose CLI runs none of brindle's hooks (``hooks_off``: the
1520
+ person's settings turn them off, at launch or later through /hooks, or
1521
+ none reported within HOOKS_GRACE of its input box appearing) has only the
1522
+ screen to go by: it may leave 'starting', a single read may move it from
1523
+ 'idle' to busy, and a ``samples`` >= 2 idle read delivers what's queued
1524
+ even when the status was already idle, since no Stop hook ever will."""
1510
1525
  provider = get_provider(agent.provider)
1511
1526
  if (agent.headless or not provider.uses_hooks
1512
1527
  or agent.status not in ("starting", "idle", "processing", "waiting")):
1513
1528
  return None # a headless pane shows output, not a TUI to read
1514
- if agent.status == "idle" and samples < 2:
1529
+ _sync_hooks_off(db, agent, provider.hooks_disabled(*_settings_scope(db, agent)))
1530
+ if agent.status == "idle" and samples < 2 and not agent.hooks_off:
1515
1531
  return None
1516
- seen = set()
1532
+ seen, ready = set(), True
1517
1533
  for i in range(samples):
1518
1534
  if i:
1519
1535
  time.sleep(gap)
@@ -1525,25 +1541,71 @@ def screen_status(db: DB, agent: Agent, samples: int = 2, gap: float = 0.7) -> s
1525
1541
  if agent.status == "idle" and state == "busy" and not provider.busy_in_footer(screen):
1526
1542
  state = None
1527
1543
  seen.add(state)
1544
+ ready = ready and provider.input_ready(screen)
1545
+ if (agent.status == "starting" and not agent.hooks_off and ready and provider.announces_start
1546
+ and time.time() - (agent.status_since or agent.created_at) >= HOOKS_GRACE):
1547
+ # Its input box is up, and SessionStart, which runs before it, never reported.
1548
+ # (A CLI without one is marked ready by after_launch instead: see ready.)
1549
+ _sync_hooks_off(db, agent, f"{NO_HOOK_EVENTS} from {provider.name}")
1528
1550
  if len(seen) != 1:
1529
1551
  return None
1530
1552
  state = seen.pop()
1531
1553
  new = {"idle": "idle", "busy": "processing", "waiting": "waiting"}.get(state or "")
1532
- if agent.status == "starting" and new != "waiting":
1554
+ if agent.status == "starting" and new != "waiting" and not agent.hooks_off:
1533
1555
  # Its hooks say when it's ready; the screen only adds that it's
1534
1556
  # stuck on a dialog before they could (e.g. folder trust).
1535
1557
  return None
1536
- if new and new != agent.status:
1558
+ changed = bool(new) and new != agent.status
1559
+ if changed:
1537
1560
  db.set_status(agent.id, new, only_if=agent.status)
1538
1561
  agent.status = new
1539
- if new == "idle" and samples >= 2 and db.pending_count(agent.id):
1540
- try:
1541
- flush(db, agent.id)
1542
- except tmux.TmuxError:
1543
- pass # stays queued; its Stop hook hands it over
1562
+ if (new == "idle" and samples >= 2 and (changed or agent.hooks_off)
1563
+ and db.pending_count(agent.id)):
1564
+ try:
1565
+ flush(db, agent.id)
1566
+ except tmux.TmuxError:
1567
+ pass # stays queued; its Stop hook hands it over
1544
1568
  return new
1545
1569
 
1546
1570
 
1571
+ # How long a hooks CLI may show its input box while still 'starting' before
1572
+ # brindle takes it that none of its hooks run (see screen_status).
1573
+ HOOKS_GRACE = 60.0
1574
+ # How hooks_off reads when no settings explain it, only the missing events.
1575
+ NO_HOOK_EVENTS = "no hook events"
1576
+
1577
+
1578
+ _settings_scopes: dict[tuple[str, float], tuple[str | None, list[str] | None]] = {}
1579
+
1580
+
1581
+ def _settings_scope(db: DB, agent: Agent) -> tuple[str | None, list[str] | None]:
1582
+ """Where ``agent``'s CLI reads its settings: its workspace, and the
1583
+ profile's setting_sources (None: all of them). Kept per agent, as the
1584
+ sidebar asks on every refresh and a running CLI keeps its launch flags."""
1585
+ key = (agent.id, agent.created_at)
1586
+ if key in _settings_scopes:
1587
+ return _settings_scopes[key]
1588
+ from brindle.profiles import ProfileError
1589
+
1590
+ ws = db.get_workspace(agent.workspace_id)
1591
+ try:
1592
+ sources = load_profile(agent.profile, ws.repo_root if ws else None).setting_sources
1593
+ except (KeyError, ProfileError):
1594
+ sources = None
1595
+ scope = _settings_scopes[key] = (ws.path if ws else None), sources
1596
+ return scope
1597
+
1598
+
1599
+ def _sync_hooks_off(db: DB, agent: Agent, why: str | None) -> None:
1600
+ """Record ``why`` its hooks are off; None clears a reason the settings
1601
+ gave (turned back on), but not missing events, which only a hook clears."""
1602
+ if why is None and agent.hooks_off and agent.hooks_off.startswith(NO_HOOK_EVENTS):
1603
+ return
1604
+ if why != agent.hooks_off:
1605
+ db.update_agent(agent.id, hooks_off=why)
1606
+ agent.hooks_off = why
1607
+
1608
+
1547
1609
  def _deliver_to_inbox(db: DB, agent: Agent, message_id: int, text: str,
1548
1610
  sender_id: str | None) -> bool:
1549
1611
  """Send a queued message through Claude Code's own inbox (see
@@ -2661,6 +2723,8 @@ def handle_hook(db: DB, agent_id: str, event: str, payload: dict) -> dict | None
2661
2723
  agent = db.get_agent(agent_id)
2662
2724
  if agent is None:
2663
2725
  return None
2726
+ if agent.hooks_off:
2727
+ db.update_agent(agent_id, hooks_off=None) # its hooks run after all: turned back on
2664
2728
  # A hook runs only once the session is going, so its launch is past any
2665
2729
  # startup prompt: a menu on its pane from now on is the agent's own (a
2666
2730
  # question for the user), which no sweep may answer.
@@ -2750,6 +2814,11 @@ def handle_hook(db: DB, agent_id: str, event: str, payload: dict) -> dict | None
2750
2814
  db.set_status(agent_id, "processing", only_if="waiting")
2751
2815
  db.set_status(agent_id, "processing", only_if="idle") # see "pre-tool"
2752
2816
  note_permission_outcome(db, agent, payload)
2817
+ from brindle import launch_health
2818
+ try:
2819
+ launch_health.check_mcp(db, agent)
2820
+ except Exception: # noqa: BLE001
2821
+ log.exception("brindle: couldn't check the brindle MCP server's start")
2753
2822
  elif event == "stop":
2754
2823
  from brindle import inbox, tasks
2755
2824
 
@@ -3052,6 +3121,47 @@ def reopen_reviewer(db: DB, agent: Agent, ws: Workspace,
3052
3121
  return True
3053
3122
 
3054
3123
 
3124
+ def review_moot(db: DB, agent: Agent) -> bool:
3125
+ """A reviewer whose branch already merged at its current commit: there is
3126
+ nothing left for it to review or report. A reviewer of commits made after
3127
+ the merge is not moot."""
3128
+ if agent.mode != "review":
3129
+ return False
3130
+ ws = db.get_workspace(agent.workspace_id)
3131
+ if ws is None:
3132
+ return False
3133
+ from brindle import pipeline
3134
+
3135
+ return (pipeline._settled(db, ws) or "").startswith(pipeline.ALREADY_MERGED)
3136
+
3137
+
3138
+ def liveness(db: DB, a: Agent, ws: Workspace | None, panes: dict[str, bool] | None = None) -> str:
3139
+ """What brindle can see of an agent for free, as one line for its
3140
+ supervisor: whether it's running, when it last did anything, and what
3141
+ its branch holds, so deciding what to do about it takes no digging.
3142
+ Never raises: a fact it can't read is left out."""
3143
+ parts = []
3144
+ try:
3145
+ parts.append("running" if is_alive(a, panes) else "not running")
3146
+ path = transcript_file(a)
3147
+ last = (os.stat(path).st_mtime if path else
3148
+ tmux.window_activity(a.tmux_window, server=server_of(a)) if a.tmux_window else None)
3149
+ if last:
3150
+ parts[-1] += f", last active {int((time.time() - last) // 60)} min ago"
3151
+ if ws is not None and os.path.isdir(ws.path):
3152
+ base = git.base_ref(ws.path, ws.base_branch or git.default_branch(ws.repo_root))
3153
+ ahead = git.ahead_behind(ws.path, base)[1]
3154
+ stat = git.out(["diff", "--stat", git.merge_base(ws.path, base)], ws.path).splitlines()
3155
+ files = ", ".join(ln.split("|")[0].strip() for ln in stat[:-1][:5])
3156
+ more = f" and {len(stat) - 6} more" if len(stat) > 6 else ""
3157
+ parts.append(f"{ahead} commit(s) ahead of {base}"
3158
+ + (f" ({files}{more}: {stat[-1].strip()})" if stat else ""))
3159
+ parts.append(f"{len(git.dirty_files(ws.path))} uncommitted file(s)")
3160
+ except Exception: # noqa: BLE001
3161
+ pass
3162
+ return "; ".join(parts)
3163
+
3164
+
3055
3165
  def tell_parent_unreported(db: DB, agent: Agent) -> bool:
3056
3166
  """A worker stopped again after being reminded to report, still without a
3057
3167
  result. It won't be reminded again on its own, and its supervisor has
@@ -3064,6 +3174,9 @@ def tell_parent_unreported(db: DB, agent: Agent) -> bool:
3064
3174
  _note_reviewer_end(db, agent)
3065
3175
  return False
3066
3176
  ws = db.get_workspace(agent.workspace_id)
3177
+ if review_moot(db, agent):
3178
+ close_later(agent.id)
3179
+ return False
3067
3180
  if agent.mode == "review" and ws is not None and os.path.isdir(ws.path):
3068
3181
  from brindle import gates
3069
3182
 
@@ -3092,9 +3205,10 @@ def tell_parent_unreported(db: DB, agent: Agent) -> bool:
3092
3205
  if agent.pipeline == "fixing" and agent.result is not None
3093
3206
  else f"stopped without calling {tool}")
3094
3207
  body = (
3095
- f"{who} {agent.id}{where} {what}, and won't be reminded "
3096
- f"again on its own. Check on it: workspace_diff to see what it did, send_message to "
3097
- f"ask it to finish or report, or remove_workspace if the work is abandoned."
3208
+ f"{who} {agent.id}{where} {what}, and won't be reminded again on its own. "
3209
+ f"What brindle sees: {liveness(db, agent, ws)}. Decide from that: send_message to "
3210
+ f"ask it to finish or report, workspace_diff for the full change, or remove_workspace "
3211
+ f"if the work is abandoned."
3098
3212
  )
3099
3213
  try:
3100
3214
  send_message(db, agent.parent_id, body)