brindle 0.3.23__tar.gz → 0.4.0__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 (149) hide show
  1. {brindle-0.3.23 → brindle-0.4.0}/PKG-INFO +89 -5
  2. {brindle-0.3.23 → brindle-0.4.0}/README.md +88 -4
  3. {brindle-0.3.23 → brindle-0.4.0}/pyproject.toml +1 -1
  4. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/agents.py +104 -53
  5. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/antigravity.py +3 -1
  6. brindle-0.4.0/src/brindle/assembler.py +43 -0
  7. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/autopilot.py +7 -4
  8. brindle-0.4.0/src/brindle/builtin_agents/coordinator.md +33 -0
  9. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/cli.py +241 -32
  10. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/cloud_login.py +45 -7
  11. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/company_identity.py +4 -1
  12. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/company_login.py +89 -13
  13. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/config.py +21 -2
  14. brindle-0.4.0/src/brindle/coordinator.py +496 -0
  15. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/cull.py +27 -3
  16. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/db.py +108 -5
  17. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/doctor.py +17 -6
  18. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/gates.py +1 -1
  19. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/history.py +1 -1
  20. brindle-0.4.0/src/brindle/kb.py +557 -0
  21. brindle-0.4.0/src/brindle/knowledge.py +493 -0
  22. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/mcp_server.py +241 -18
  23. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/picker.py +52 -1
  24. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/pipeline.py +138 -17
  25. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/pro/account.py +2 -0
  26. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/pro/license.py +11 -7
  27. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/prompt_answer.py +17 -2
  28. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/providers.py +40 -4
  29. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/sessions.py +44 -1
  30. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/sidebar_follow.py +11 -0
  31. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/sidebar_logo.py +10 -0
  32. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/signin_pause.py +74 -6
  33. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/tasks.py +248 -28
  34. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/tmux.py +54 -21
  35. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/upgrade.py +20 -4
  36. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/view.py +2 -1
  37. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/watch.py +25 -5
  38. {brindle-0.3.23 → brindle-0.4.0}/.gitignore +0 -0
  39. {brindle-0.3.23 → brindle-0.4.0}/LICENSE +0 -0
  40. {brindle-0.3.23 → brindle-0.4.0}/SCHEDULE-A +0 -0
  41. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/__init__.py +0 -0
  42. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/__main__.py +0 -0
  43. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/account.py +0 -0
  44. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/airgap.py +0 -0
  45. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/assets/braid_dark.png +0 -0
  46. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/assets/braid_light.png +0 -0
  47. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/budget.py +0 -0
  48. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/builtin_agents/__init__.py +0 -0
  49. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/builtin_agents/backend-auditor.md +0 -0
  50. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/builtin_agents/developer-codex.md +0 -0
  51. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/builtin_agents/developer-fable.md +0 -0
  52. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/builtin_agents/developer-gemini.md +0 -0
  53. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/builtin_agents/developer-heavy.md +0 -0
  54. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/builtin_agents/developer-light.md +0 -0
  55. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/builtin_agents/developer-local.md +0 -0
  56. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/builtin_agents/developer.md +0 -0
  57. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/builtin_agents/qa.md +0 -0
  58. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/builtin_agents/reviewer-codex.md +0 -0
  59. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/builtin_agents/reviewer-gemini.md +0 -0
  60. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/builtin_agents/reviewer-local.md +0 -0
  61. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/builtin_agents/reviewer.md +0 -0
  62. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/builtin_agents/subagent.md +0 -0
  63. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/builtin_agents/supervisor.md +0 -0
  64. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/builtin_rules/__init__.py +0 -0
  65. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/builtin_rules/security/backend.md +0 -0
  66. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/builtin_rules/style/minimal-diff.md +0 -0
  67. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/builtin_rules/tests/only.md +0 -0
  68. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/ci_adapters.py +0 -0
  69. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/ci_client.py +0 -0
  70. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/ci_federation.py +0 -0
  71. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/ci_hosts.py +0 -0
  72. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/cloud_credential.py +0 -0
  73. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/codemap.py +0 -0
  74. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/codemap_docs.py +0 -0
  75. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/codex_hook.py +0 -0
  76. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/conflicts.py +0 -0
  77. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/cost.py +0 -0
  78. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/cost_estimate.py +0 -0
  79. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/demo.py +0 -0
  80. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/detect.py +0 -0
  81. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/escapes.py +0 -0
  82. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/events.py +0 -0
  83. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/git.py +0 -0
  84. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/guardrails.py +0 -0
  85. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/identity_lock.py +0 -0
  86. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/inbox.py +0 -0
  87. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/key_check.py +0 -0
  88. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/keys_cmds.py +0 -0
  89. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/keystore.py +0 -0
  90. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/launch_health.py +0 -0
  91. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/learned_rules.py +0 -0
  92. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/learning.py +0 -0
  93. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/legacy.py +0 -0
  94. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/local_setup.py +0 -0
  95. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/login_pause.py +0 -0
  96. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/model_access.py +0 -0
  97. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/model_availability.py +0 -0
  98. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/native/__init__.py +0 -0
  99. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/native/client.py +0 -0
  100. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/native/loop.py +0 -0
  101. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/native/permissions.py +0 -0
  102. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/native/runner.py +0 -0
  103. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/native/serve.py +0 -0
  104. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/native/tools.py +0 -0
  105. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/pane_auth.py +0 -0
  106. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/permissions.py +0 -0
  107. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/plugins.py +0 -0
  108. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/policy.py +0 -0
  109. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/pool.py +0 -0
  110. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/pricing.py +0 -0
  111. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/pro/__init__.py +0 -0
  112. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/pro/_files.py +0 -0
  113. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/pro/audit_chain.py +0 -0
  114. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/pro/audit_export.py +0 -0
  115. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/pro/auth.py +0 -0
  116. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/pro/brief_learning.py +0 -0
  117. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/pro/cost_centers.py +0 -0
  118. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/pro/credentials.py +0 -0
  119. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/pro/features.py +0 -0
  120. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/pro/keys.py +0 -0
  121. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/pro/learning.py +0 -0
  122. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/pro/loopback.py +0 -0
  123. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/pro/managed_models.py +0 -0
  124. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/pro/org_profiles.py +0 -0
  125. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/pro/orgkey.py +0 -0
  126. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/pro/rollout.py +0 -0
  127. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/pro/route_health.py +0 -0
  128. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/pro/settings_sync.py +0 -0
  129. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/pro/status.py +0 -0
  130. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/pro/team_events.py +0 -0
  131. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/pro/team_policy.py +0 -0
  132. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/procs.py +0 -0
  133. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/profiles.py +0 -0
  134. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/quota.py +0 -0
  135. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/repo_cmds.py +0 -0
  136. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/repos.py +0 -0
  137. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/rewind.py +0 -0
  138. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/rule_checks.py +0 -0
  139. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/savings.py +0 -0
  140. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/scratch.py +0 -0
  141. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/secrets.py +0 -0
  142. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/self_serve.py +0 -0
  143. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/services.py +0 -0
  144. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/status_cache.py +0 -0
  145. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/updates.py +0 -0
  146. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/usage.py +0 -0
  147. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/watch_costs.py +0 -0
  148. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/worker_models.py +0 -0
  149. {brindle-0.3.23 → brindle-0.4.0}/src/brindle/workspaces.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: brindle
3
- Version: 0.3.23
3
+ Version: 0.4.0
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.23`). A session started before an upgrade keeps
97
+ session shows it too (`brindle 0.4.0`). 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
@@ -193,7 +193,7 @@ and you're back at your prompt. The whole session is paused: its
193
193
  workers stop too, and everything is kept (branches, worktrees,
194
194
  queued messages, and each agent's Claude conversation). `brindle continue` (or
195
195
  `brindle -c`) picks up the most recent paused session and lists the others by id
196
- (`brindle continue <id>`). Plain `brindle` always starts fresh. If a session is still running in that folder, it asks first: open that one, start the new one in its own worktree (branch `brindle/session-N`, cut from what you have checked out, so they run at once without sharing files), or pause it and start fresh. There's no limit: each further `brindle` there can start another session in its own worktree (`brindle/session-2`, `-3`, ...). Without a terminal to ask in, it pauses the old one. `brindle sessions` lists
196
+ (`brindle continue <id>`). Plain `brindle` always starts fresh. If a session is still running in that folder, it asks first: open that one, start the new one in its own worktree (branch `brindle/session-N`, cut from what you have checked out, so they run at once without sharing files), or pause it and start fresh. There's no limit: each further `brindle` there can start another session in its own worktree (`brindle/session-2`, `-3`, ...). Without a terminal to ask in, it pauses the old one. When sessions are running in the repo, in any of its worktrees, that question is a list: one row each with the session's id, goal, branch, age and running workers. Pick one to open it, `n` for a new session in its own worktree, or `p` to pause the picked one and start fresh. With two or more paused sessions, `brindle -c` lists them newest first (Enter resumes the newest). `brindle sessions` lists
197
197
  what's paused. brindle keeps the newest 3 paused sessions per repo for up to 7 days;
198
198
  cleanup never merges anything or deletes an unmerged branch, and worktrees with uncommitted
199
199
  changes are kept.
@@ -288,7 +288,7 @@ or put the goal in `.brindle/goals.md`, and it works like a project manager:
288
288
  they keep going until it's met. `assign`/`handoff` take `files` (the
289
289
  paths/globs a task expects to touch, checked for overlap against other
290
290
  active workers' declared and actually-changed files; refused by default,
291
- a warning with `"overlap": "warn"`) and `depends_on` (earlier tasks, by agent id or branch, that must
291
+ queued behind another session's task, a warning with `"overlap": "warn"`) and `depends_on` (earlier tasks, by agent id or branch, that must
292
292
  merge first): a task with unmet dependencies is queued instead of started,
293
293
  and starts automatically, cut from the updated base, once
294
294
  `merge_workspace` resolves them. `list_tasks` shows what's queued.
@@ -413,6 +413,9 @@ your own status line prints, so what you see doesn't change.
413
413
  | `brindle profile new/lint/show` | write a profile file, check every profile (extends chains, rule packs, patterns), or print one as brindle resolves it |
414
414
  | `brindle rules suggest/accept/reject` | (brindle Pro) rules suggested from review findings that keep recurring; accept one into `.brindle/rules/learned.md`, or reject it for good |
415
415
  | `brindle agent turns AGENT` / `brindle agent rewind AGENT --to N [--profile P] [--note "..."]` | a worker's worktree is snapshotted after every turn that changed files; rewind puts it back as it was after turn N and starts a fresh session there, briefed on the task, turns 1..N and your note (any provider, Codex included). Supervisors have the `agent_turns` and `rewind_agent` tools |
416
+ | `brindle kb add folder PATH` / `kb list` / `kb sync [SOURCE]` / `kb remove SOURCE` | the documents agents can search for this repo (see "Shared knowledge across sessions") |
417
+ | `brindle decisions export [FILE]` | this repo's accepted decisions as Markdown (stdout by default) |
418
+ | `brindle coordinator log [-n N]` / `undo ID` / `stop` | Pro: what the coordinator did, put one action back, or switch it off (see "The coordinator") |
416
419
  | `brindle setup [WS]` | re-run the repo's setup commands in a workspace |
417
420
  | `brindle mcp` | the MCP server agents talk to (launched for them; you don't run it) |
418
421
 
@@ -441,6 +444,9 @@ knowing them helps when you tell the supervisor how to work.
441
444
  | `complete_subagent` | supervisor | record the result of a `subagent`-profile task |
442
445
  | `set_goal` / `get_progress` / `check_milestone` | supervisor | autopilot's goal, its progress, and running the checks |
443
446
  | `record_audit_decision` | supervisor | record acceptance of an audit finding with rationale so subsequent audits respect it |
447
+ | `who_is_working` | supervisor | other sessions in this repo: their goals and running tasks, optionally only those matching `paths` or a `query` |
448
+ | `record_decision` / `decisions` | supervisor (`decisions` also workers) | record a durable decision about the system / list the accepted ones that apply |
449
+ | `kb_search` | supervisor, worker | search the repo's knowledge base and accepted decisions |
444
450
  | `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 |
445
451
  | `transfer_to_repo` | supervisor | move a scratch session's work into a repository |
446
452
  | `handover` | supervisor | hand the session to a new supervisor on another branch or worktree, with a note |
@@ -511,6 +517,84 @@ reported to the supervisor at 80% of `task_usd` and again at 100%; set
511
517
  `"stop": true` to close a worker at 100% instead of only warning. Without the
512
518
  Pro `cost` feature budgets do nothing.
513
519
 
520
+ ## Shared knowledge across sessions
521
+
522
+ Sessions on one repo share a small store in `~/.brindle/brindle.db`, keyed by
523
+ the repo's remote (two clones of one repo share it). It holds what each session
524
+ plans, the decisions you've recorded and the documents you've added. Nothing
525
+ leaves your machine.
526
+
527
+ ### Other sessions
528
+
529
+ Each session announces what it's working on when its supervisor sets a goal.
530
+ When a new task's `files` overlap another session's running task, brindle
531
+ never touches that session's work: the task is queued (the default; see
532
+ `overlap` in the repo config), both supervisors get a notice naming the other
533
+ session's goal, and the task starts, cut from the commit holding that session's
534
+ merge, once it merges. Each supervisor also sees an "Other sessions in this
535
+ repo" summary, and the `who_is_working` tool lists their goals and running
536
+ tasks, optionally only those matching some paths. A paused session's goal
537
+ stays visible, marked paused, until retention drops it.
538
+
539
+ ### Decisions
540
+
541
+ A decision is a fact about the system ("config secrets live in the OS
542
+ keychain"), not a working rule; those stay in learned rules. A supervisor
543
+ records one with `record_decision` (topic, decision, rationale, optional path
544
+ globs) and reads them with `decisions`. Recording one that overlaps an accepted
545
+ decision on the same topic is refused and shows the existing one: it has to
546
+ name it as superseded, so two sessions can't quietly contradict each other.
547
+ Supervisors whose running tasks touch the paths are told. Accepted decisions
548
+ whose paths match a task's `files` go into that worker's brief, and a resumed
549
+ worker gets the brief it started with.
550
+
551
+ ```sh
552
+ brindle decisions export # accepted decisions as Markdown on stdout
553
+ brindle decisions export DECISIONS.md # or into a file you can commit
554
+ ```
555
+
556
+ ### Knowledge base
557
+
558
+ Add the documents a person reads first (ADRs, runbooks, design notes) and
559
+ agents can search them:
560
+
561
+ ```sh
562
+ brindle kb add folder docs/ # Markdown and text files, indexed now
563
+ brindle kb list # sources, documents, quarantined chunks
564
+ brindle kb sync # pick up changed files, drop deleted ones
565
+ brindle kb remove docs/ # the source and everything indexed from it
566
+ ```
567
+
568
+ Supervisors and workers search with `kb_search`, which returns the matching
569
+ passages with their source and date, beside the accepted decisions. When
570
+ brindle starts a worker it also searches with the task and its `files` and
571
+ puts the best few passages (about 1,500 tokens) in the brief, under a header
572
+ that marks them as untrusted reference data, never instructions, and labelled
573
+ stale when old. Passages that name functions or classes in the repo's code
574
+ graph come first for tasks touching those files.
575
+
576
+ Text is checked as it's indexed: strings that look like secrets are redacted,
577
+ and passages that address an AI, hide text or hold instructions in HTML
578
+ comments are quarantined and never returned. Review before merge is unchanged
579
+ for work that used them. The index lives under `~/.brindle`, never in the repo.
580
+
581
+ ### The coordinator (Pro)
582
+
583
+ Set `"coordinator": true` in `.brindle/config.json` and brindle runs one cheap
584
+ agent per repo above the supervisors. It wakes on events only (a new goal, a
585
+ clash notice, a decision, a task queued over 20 minutes), within your budget.
586
+ It may message supervisors, hold, release or reorder queued tasks from any
587
+ session, and propose decisions; you still accept those. It never cancels or
588
+ pauses running work, merges, accepts a decision or edits code: for running work
589
+ it can only ask the owning supervisor. Every action is logged with its reason
590
+ and shown to the supervisors it affects.
591
+
592
+ ```sh
593
+ brindle coordinator log # recent actions and reports, newest first
594
+ brindle coordinator undo 3f9c2a1b # put one action back (the id is in the notice)
595
+ brindle coordinator stop # close it and turn it off for this checkout
596
+ ```
597
+
514
598
  ## Provider quota
515
599
 
516
600
  brindle keeps one place (`~/.brindle/quota.json`) that knows how close each
@@ -577,7 +661,7 @@ Autopilot, merge gates and cleanup:
577
661
  | `agy_approvals` | `"prompt"` | `"brindle"`: with the policy on, Antigravity workers run without approval prompts and brindle's hook allows or denies every call (see "Antigravity" under "Permission policy") |
578
662
  | `rules` | `[]` | standing rules for the supervisor, e.g. `["Fix review findings without asking", "Validate options before offering them"]`; added to every supervisor's brief. Rules in `~/.brindle/config.json`, the repo's config and `config.local.json` are all kept, so a team's rules and your own add up |
579
663
  | `plan_first` | `false` | workers propose a plan (`submit_plan`) and wait for `approve_plan` before editing |
580
- | `overlap` | `"block"` | a task whose `files` overlap a running task's is refused (`"warn"` starts it with a warning) |
664
+ | `overlap` | unset | a task whose `files` overlap a running task's: `"block"` refuses it, `"warn"` starts it with a warning, `"queue"` queues it until that task merges. Unset, it is refused within a session and queued behind another session's task (both sessions' supervisors are told; the other session's work is never cancelled), and the queued task starts from that task's merge commit |
581
665
  | `protected_paths` | `[]` | paths or globs (`"migrations/"`, `"*.lock"`) whose merge conflicts brindle never hands to a worker to resolve: the branch waits for you instead (see "Conflict-aware merging") |
582
666
  | `pool_size` | `1` if `setup` is set, else `0` | pre-built worktrees (checked out, files copied, setup run) kept ready so a new worker doesn't wait on `setup`; `0` disables it |
583
667
  | `add_dirs` | `[]` | directories outside the worktree that Claude Code agents may use (`--add-dir`; full tool access, see "Directories outside the workspace") |
@@ -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.23`). A session started before an upgrade keeps
66
+ session shows it too (`brindle 0.4.0`). 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
@@ -162,7 +162,7 @@ and you're back at your prompt. The whole session is paused: its
162
162
  workers stop too, and everything is kept (branches, worktrees,
163
163
  queued messages, and each agent's Claude conversation). `brindle continue` (or
164
164
  `brindle -c`) picks up the most recent paused session and lists the others by id
165
- (`brindle continue <id>`). Plain `brindle` always starts fresh. If a session is still running in that folder, it asks first: open that one, start the new one in its own worktree (branch `brindle/session-N`, cut from what you have checked out, so they run at once without sharing files), or pause it and start fresh. There's no limit: each further `brindle` there can start another session in its own worktree (`brindle/session-2`, `-3`, ...). Without a terminal to ask in, it pauses the old one. `brindle sessions` lists
165
+ (`brindle continue <id>`). Plain `brindle` always starts fresh. If a session is still running in that folder, it asks first: open that one, start the new one in its own worktree (branch `brindle/session-N`, cut from what you have checked out, so they run at once without sharing files), or pause it and start fresh. There's no limit: each further `brindle` there can start another session in its own worktree (`brindle/session-2`, `-3`, ...). Without a terminal to ask in, it pauses the old one. When sessions are running in the repo, in any of its worktrees, that question is a list: one row each with the session's id, goal, branch, age and running workers. Pick one to open it, `n` for a new session in its own worktree, or `p` to pause the picked one and start fresh. With two or more paused sessions, `brindle -c` lists them newest first (Enter resumes the newest). `brindle sessions` lists
166
166
  what's paused. brindle keeps the newest 3 paused sessions per repo for up to 7 days;
167
167
  cleanup never merges anything or deletes an unmerged branch, and worktrees with uncommitted
168
168
  changes are kept.
@@ -257,7 +257,7 @@ or put the goal in `.brindle/goals.md`, and it works like a project manager:
257
257
  they keep going until it's met. `assign`/`handoff` take `files` (the
258
258
  paths/globs a task expects to touch, checked for overlap against other
259
259
  active workers' declared and actually-changed files; refused by default,
260
- a warning with `"overlap": "warn"`) and `depends_on` (earlier tasks, by agent id or branch, that must
260
+ queued behind another session's task, a warning with `"overlap": "warn"`) and `depends_on` (earlier tasks, by agent id or branch, that must
261
261
  merge first): a task with unmet dependencies is queued instead of started,
262
262
  and starts automatically, cut from the updated base, once
263
263
  `merge_workspace` resolves them. `list_tasks` shows what's queued.
@@ -382,6 +382,9 @@ your own status line prints, so what you see doesn't change.
382
382
  | `brindle profile new/lint/show` | write a profile file, check every profile (extends chains, rule packs, patterns), or print one as brindle resolves it |
383
383
  | `brindle rules suggest/accept/reject` | (brindle Pro) rules suggested from review findings that keep recurring; accept one into `.brindle/rules/learned.md`, or reject it for good |
384
384
  | `brindle agent turns AGENT` / `brindle agent rewind AGENT --to N [--profile P] [--note "..."]` | a worker's worktree is snapshotted after every turn that changed files; rewind puts it back as it was after turn N and starts a fresh session there, briefed on the task, turns 1..N and your note (any provider, Codex included). Supervisors have the `agent_turns` and `rewind_agent` tools |
385
+ | `brindle kb add folder PATH` / `kb list` / `kb sync [SOURCE]` / `kb remove SOURCE` | the documents agents can search for this repo (see "Shared knowledge across sessions") |
386
+ | `brindle decisions export [FILE]` | this repo's accepted decisions as Markdown (stdout by default) |
387
+ | `brindle coordinator log [-n N]` / `undo ID` / `stop` | Pro: what the coordinator did, put one action back, or switch it off (see "The coordinator") |
385
388
  | `brindle setup [WS]` | re-run the repo's setup commands in a workspace |
386
389
  | `brindle mcp` | the MCP server agents talk to (launched for them; you don't run it) |
387
390
 
@@ -410,6 +413,9 @@ knowing them helps when you tell the supervisor how to work.
410
413
  | `complete_subagent` | supervisor | record the result of a `subagent`-profile task |
411
414
  | `set_goal` / `get_progress` / `check_milestone` | supervisor | autopilot's goal, its progress, and running the checks |
412
415
  | `record_audit_decision` | supervisor | record acceptance of an audit finding with rationale so subsequent audits respect it |
416
+ | `who_is_working` | supervisor | other sessions in this repo: their goals and running tasks, optionally only those matching `paths` or a `query` |
417
+ | `record_decision` / `decisions` | supervisor (`decisions` also workers) | record a durable decision about the system / list the accepted ones that apply |
418
+ | `kb_search` | supervisor, worker | search the repo's knowledge base and accepted decisions |
413
419
  | `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 |
414
420
  | `transfer_to_repo` | supervisor | move a scratch session's work into a repository |
415
421
  | `handover` | supervisor | hand the session to a new supervisor on another branch or worktree, with a note |
@@ -480,6 +486,84 @@ reported to the supervisor at 80% of `task_usd` and again at 100%; set
480
486
  `"stop": true` to close a worker at 100% instead of only warning. Without the
481
487
  Pro `cost` feature budgets do nothing.
482
488
 
489
+ ## Shared knowledge across sessions
490
+
491
+ Sessions on one repo share a small store in `~/.brindle/brindle.db`, keyed by
492
+ the repo's remote (two clones of one repo share it). It holds what each session
493
+ plans, the decisions you've recorded and the documents you've added. Nothing
494
+ leaves your machine.
495
+
496
+ ### Other sessions
497
+
498
+ Each session announces what it's working on when its supervisor sets a goal.
499
+ When a new task's `files` overlap another session's running task, brindle
500
+ never touches that session's work: the task is queued (the default; see
501
+ `overlap` in the repo config), both supervisors get a notice naming the other
502
+ session's goal, and the task starts, cut from the commit holding that session's
503
+ merge, once it merges. Each supervisor also sees an "Other sessions in this
504
+ repo" summary, and the `who_is_working` tool lists their goals and running
505
+ tasks, optionally only those matching some paths. A paused session's goal
506
+ stays visible, marked paused, until retention drops it.
507
+
508
+ ### Decisions
509
+
510
+ A decision is a fact about the system ("config secrets live in the OS
511
+ keychain"), not a working rule; those stay in learned rules. A supervisor
512
+ records one with `record_decision` (topic, decision, rationale, optional path
513
+ globs) and reads them with `decisions`. Recording one that overlaps an accepted
514
+ decision on the same topic is refused and shows the existing one: it has to
515
+ name it as superseded, so two sessions can't quietly contradict each other.
516
+ Supervisors whose running tasks touch the paths are told. Accepted decisions
517
+ whose paths match a task's `files` go into that worker's brief, and a resumed
518
+ worker gets the brief it started with.
519
+
520
+ ```sh
521
+ brindle decisions export # accepted decisions as Markdown on stdout
522
+ brindle decisions export DECISIONS.md # or into a file you can commit
523
+ ```
524
+
525
+ ### Knowledge base
526
+
527
+ Add the documents a person reads first (ADRs, runbooks, design notes) and
528
+ agents can search them:
529
+
530
+ ```sh
531
+ brindle kb add folder docs/ # Markdown and text files, indexed now
532
+ brindle kb list # sources, documents, quarantined chunks
533
+ brindle kb sync # pick up changed files, drop deleted ones
534
+ brindle kb remove docs/ # the source and everything indexed from it
535
+ ```
536
+
537
+ Supervisors and workers search with `kb_search`, which returns the matching
538
+ passages with their source and date, beside the accepted decisions. When
539
+ brindle starts a worker it also searches with the task and its `files` and
540
+ puts the best few passages (about 1,500 tokens) in the brief, under a header
541
+ that marks them as untrusted reference data, never instructions, and labelled
542
+ stale when old. Passages that name functions or classes in the repo's code
543
+ graph come first for tasks touching those files.
544
+
545
+ Text is checked as it's indexed: strings that look like secrets are redacted,
546
+ and passages that address an AI, hide text or hold instructions in HTML
547
+ comments are quarantined and never returned. Review before merge is unchanged
548
+ for work that used them. The index lives under `~/.brindle`, never in the repo.
549
+
550
+ ### The coordinator (Pro)
551
+
552
+ Set `"coordinator": true` in `.brindle/config.json` and brindle runs one cheap
553
+ agent per repo above the supervisors. It wakes on events only (a new goal, a
554
+ clash notice, a decision, a task queued over 20 minutes), within your budget.
555
+ It may message supervisors, hold, release or reorder queued tasks from any
556
+ session, and propose decisions; you still accept those. It never cancels or
557
+ pauses running work, merges, accepts a decision or edits code: for running work
558
+ it can only ask the owning supervisor. Every action is logged with its reason
559
+ and shown to the supervisors it affects.
560
+
561
+ ```sh
562
+ brindle coordinator log # recent actions and reports, newest first
563
+ brindle coordinator undo 3f9c2a1b # put one action back (the id is in the notice)
564
+ brindle coordinator stop # close it and turn it off for this checkout
565
+ ```
566
+
483
567
  ## Provider quota
484
568
 
485
569
  brindle keeps one place (`~/.brindle/quota.json`) that knows how close each
@@ -546,7 +630,7 @@ Autopilot, merge gates and cleanup:
546
630
  | `agy_approvals` | `"prompt"` | `"brindle"`: with the policy on, Antigravity workers run without approval prompts and brindle's hook allows or denies every call (see "Antigravity" under "Permission policy") |
547
631
  | `rules` | `[]` | standing rules for the supervisor, e.g. `["Fix review findings without asking", "Validate options before offering them"]`; added to every supervisor's brief. Rules in `~/.brindle/config.json`, the repo's config and `config.local.json` are all kept, so a team's rules and your own add up |
548
632
  | `plan_first` | `false` | workers propose a plan (`submit_plan`) and wait for `approve_plan` before editing |
549
- | `overlap` | `"block"` | a task whose `files` overlap a running task's is refused (`"warn"` starts it with a warning) |
633
+ | `overlap` | unset | a task whose `files` overlap a running task's: `"block"` refuses it, `"warn"` starts it with a warning, `"queue"` queues it until that task merges. Unset, it is refused within a session and queued behind another session's task (both sessions' supervisors are told; the other session's work is never cancelled), and the queued task starts from that task's merge commit |
550
634
  | `protected_paths` | `[]` | paths or globs (`"migrations/"`, `"*.lock"`) whose merge conflicts brindle never hands to a worker to resolve: the branch waits for you instead (see "Conflict-aware merging") |
551
635
  | `pool_size` | `1` if `setup` is set, else `0` | pre-built worktrees (checked out, files copied, setup run) kept ready so a new worker doesn't wait on `setup`; `0` disables it |
552
636
  | `add_dirs` | `[]` | directories outside the worktree that Claude Code agents may use (`--add-dir`; full tool access, see "Directories outside the workspace") |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "brindle"
3
- version = "0.3.23"
3
+ version = "0.4.0"
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"
@@ -21,7 +21,8 @@ import time
21
21
  import uuid
22
22
  from types import SimpleNamespace
23
23
 
24
- from brindle import cloud_credential, company_identity, company_login, git, keystore, pane_auth, providers, secrets, tmux, workspaces
24
+ from brindle import cloud_credential, company_identity, company_login, git, keystore, knowledge, pane_auth, providers, secrets, tmux, workspaces
25
+ from brindle.assembler import DECISIONS, DOCUMENT_CHUNKS, OTHER_SESSIONS, TASK_AND_RULES, Block, assemble
25
26
  from brindle.config import RepoConfig
26
27
  from brindle.db import DB, Agent, Workspace
27
28
  from brindle.model_availability import ModelAvailabilityChecker, PROVISIONING_TIMEOUT, provisioning_context
@@ -286,15 +287,18 @@ plan is approved; if it comes back with feedback, revise it and call
286
287
 
287
288
 
288
289
  def decorate_worker_prompt(task: str, agent_id: str, ws: Workspace, done_when: str | None,
289
- provider, headless: bool, plan_first: bool = False) -> str:
290
+ provider, headless: bool, plan_first: bool = False,
291
+ decisions: str = "", chunks: str = "") -> str:
290
292
  """The prompt actually sent to a handoff/assign worker's CLI: the raw
291
- ``task`` plus its finish line, the WORKER_FOOTER reminder to report, and
292
- (for an interactive Claude worker with a finish line) the ``/goal``
293
- wrapper. Used at spawn time, and again by ``resume`` to rebuild it when a
294
- paused worker's CLI session can't be resumed and must restart fresh."""
293
+ ``task``, the ``decisions`` block that covers its files, its finish line,
294
+ the WORKER_FOOTER reminder to report, and (for an interactive Claude
295
+ worker with a finish line) the ``/goal`` wrapper. Used at spawn time, and
296
+ again by ``resume`` to rebuild it (decisions from the brief's receipt)
297
+ when a paused worker's CLI session can't be resumed and must restart fresh."""
295
298
  from brindle import autopilot as pilot
296
299
 
297
- prompt = task
300
+ prompt = assemble([Block(TASK_AND_RULES, task), Block(DECISIONS, decisions),
301
+ Block(DOCUMENT_CHUNKS, chunks)])
298
302
  if done_when:
299
303
  prompt += f"\n\nFinish line: {done_when.strip()}"
300
304
  if plan_first:
@@ -326,9 +330,11 @@ def spawn(
326
330
  plan_first: bool = False,
327
331
  review_sha: str | None = None,
328
332
  claim_goals: bool = True,
333
+ files: list[str] | None = None,
329
334
  ) -> Agent:
330
335
  """Start an agent in ``ws``. Workers (handoff/assign) given a ``done_when``
331
- finish line run it as a Claude Code ``/goal``. With ``autopilot``, the
336
+ finish line run it as a Claude Code ``/goal``; their brief carries the
337
+ accepted decisions covering ``files``. With ``autopilot``, the
332
338
  agent is a session root that drives toward a goal (see brindle.autopilot);
333
339
  with ``claim_goals`` it claims a hand-written ``.brindle/goals.md``."""
334
340
  from brindle import autopilot as pilot
@@ -336,6 +342,10 @@ def spawn(
336
342
 
337
343
  identity_lock.ensure_workspace(db, ws) # no new agent in another identity's worktree
338
344
  profile = load_profile(profile_name, ws.repo_root)
345
+ if (profile.name == "coordinator") != (mode == "coordinator"):
346
+ # Only the sweep starts a coordinator (brindle.coordinator.wake).
347
+ raise AgentError("the coordinator profile only runs as the repo's coordinator, which "
348
+ "brindle starts itself; pick another profile")
339
349
  hosted = local_fallback(profile, ws.repo_root) # its local model isn't up
340
350
  if hosted:
341
351
  profile_name, profile = hosted, load_profile(hosted, ws.repo_root)
@@ -366,8 +376,12 @@ def spawn(
366
376
  full = f"{profile.prompt.strip()}\n\n{rules}".strip() if rules else profile.prompt
367
377
  prompt = raw_task = subagent_prompt(full, prompt or "", ws, done_when)
368
378
  elif prompt and mode in ("handoff", "assign"):
379
+ from brindle import codemap
369
380
  prompt = decorate_worker_prompt(prompt, agent_id, ws, done_when, provider, headless,
370
- plan_first=plan_first)
381
+ plan_first=plan_first,
382
+ decisions=knowledge.brief_decisions(db, ws.repo_root, agent_id, files),
383
+ chunks=knowledge.brief_chunks(db, ws.repo_root, agent_id, prompt, files,
384
+ codemap.graph_path(ws)))
371
385
  elif prompt and mode == "review":
372
386
  prompt = raw_task = prompt + REVIEW_FOOTER.format(agent_id=agent_id, branch=ws.branch)
373
387
 
@@ -448,8 +462,8 @@ def _profile_for(db: DB, agent: Agent, ws: Workspace, m=_UNREAD):
448
462
  # The profile's rule packs: their text joins the prompt here, for every
449
463
  # provider; their mechanical rules are checked by brindle.rule_checks.
450
464
  # So do its guardrails (write and read scope), which that check enforces.
451
- rules = "\n\n".join(p for p in (rules_prompt(load_rule_packs(profile, ws.repo_root)),
452
- guardrails.scope_prompt(profile)) if p)
465
+ rules = assemble([Block(TASK_AND_RULES, rules_prompt(load_rule_packs(profile, ws.repo_root))),
466
+ Block(TASK_AND_RULES, guardrails.scope_prompt(profile))])
453
467
  if rules:
454
468
  profile = replace(profile, prompt=f"{profile.prompt.strip()}\n\n{rules}".strip())
455
469
  cfg = load_repo_config(ws.repo_root)
@@ -462,15 +476,12 @@ def _profile_for(db: DB, agent: Agent, ws: Workspace, m=_UNREAD):
462
476
  if agent.mode == "interactive":
463
477
  from brindle import codemap, repos
464
478
 
465
- note = codemap.guidance(ws)
466
- if note:
467
- profile = replace(profile, prompt=f"{profile.prompt}\n\n{note}".strip())
468
479
  repo_map = codemap.supervisor_map(ws) if autopilot_on or agent.profile == "supervisor" else None
469
- if repo_map:
470
- profile = replace(profile, prompt=f"{profile.prompt}\n\n{repo_map}".strip())
471
- attached = repos.guidance(db, agent.id) # a session root's attached repos
472
- if attached:
473
- profile = replace(profile, prompt=f"{profile.prompt}\n\n{attached}".strip())
480
+ extra = assemble([Block(TASK_AND_RULES, (text or "").rstrip()) for text in (
481
+ codemap.guidance(ws), repo_map, repos.guidance(db, agent.id))] # attached repos: a session root's
482
+ + [Block(OTHER_SESSIONS, knowledge.other_sessions_block(db, knowledge.repo_key(ws.repo_root), agent.id))])
483
+ if extra:
484
+ profile = replace(profile, prompt=f"{profile.prompt}\n\n{extra}".strip())
474
485
  if agent.headless:
475
486
  profile = replace(profile, headless=True)
476
487
  # An org's managed models (Enterprise): a profile pointing elsewhere is
@@ -1156,6 +1167,7 @@ def pause(db: DB, root_id: str, *, stop_procs: bool = True,
1156
1167
  tmux.kill_session(session)
1157
1168
  if stop_local_models:
1158
1169
  _stop_local_models(db)
1170
+ knowledge.note(db, root_id, knowledge.PAUSE)
1159
1171
  return paused
1160
1172
 
1161
1173
 
@@ -1253,7 +1265,9 @@ def resume(db: DB, root_id: str, *, watch_pane: bool = True,
1253
1265
  # it gets decorated a second time here; harmless, if redundant.
1254
1266
  prompt = decorate_worker_prompt(
1255
1267
  a.task, a.id, ws, a.done_when, provider, bool(a.headless),
1256
- plan_first=bool(a.plan_first) and a.plan_state != "approved") + RESUME_NOTE
1268
+ plan_first=bool(a.plan_first) and a.plan_state != "approved",
1269
+ decisions=knowledge.replay_decisions(db, a.id),
1270
+ chunks=knowledge.replay_chunks(db, a.id)) + RESUME_NOTE
1257
1271
  else:
1258
1272
  prompt = (a.task + RESUME_NOTE) if a.task else None
1259
1273
  if a.dismissed_at is not None:
@@ -1261,6 +1275,8 @@ def resume(db: DB, root_id: str, *, watch_pane: bool = True,
1261
1275
  _launch(db, a, ws, prompt=prompt, resume=ref,
1262
1276
  watch_pane=watch_pane and a.id == root_id, background_setup=True)
1263
1277
  resumed.append(a)
1278
+ if any(a.id == root_id for a in resumed):
1279
+ knowledge.note(db, root_id, knowledge.RESUME)
1264
1280
  ap = db.get_autopilot(root_id)
1265
1281
  failed_still = [a for a in candidates if db.get_agent(a.id) and db.get_agent(a.id).status == "failed"]
1266
1282
  if ap and ap.state == "blocked" and resumed and not failed_still:
@@ -1358,6 +1374,24 @@ def is_alive(agent: Agent, panes: dict[str, bool] | None = None) -> bool:
1358
1374
  return tmux.window_alive(agent.tmux_window, server=agent.tmux_server or None)
1359
1375
 
1360
1376
 
1377
+ LAUNCH_GRACE = 60.0 # seconds a just-spawned agent may go without a window before it counts as dead
1378
+
1379
+
1380
+ def alive_or_starting(agent: Agent) -> bool:
1381
+ """``is_alive``, or still being launched: spawn records the agent before
1382
+ its window opens (the launch's checks can take seconds), so one with no
1383
+ window yet is starting, not dead, for LAUNCH_GRACE seconds. A launch known
1384
+ to have failed ends that: spawn deletes the row of one that raised, and
1385
+ launch_health marks one whose CLI died at startup "failed". For the
1386
+ checks that would replace the agent (start another reviewer, forget an
1387
+ auditor), not for ones that talk to its pane."""
1388
+ if is_alive(agent):
1389
+ return True
1390
+ return (runs_process(agent) and not agent.tmux_window and agent.result is None
1391
+ and agent.status not in ("paused", "done", "failed")
1392
+ and time.time() - agent.created_at < LAUNCH_GRACE)
1393
+
1394
+
1361
1395
  def server_of(agent: Agent) -> str | None:
1362
1396
  """The tmux server ``agent``'s window is on (a private socket name, or
1363
1397
  None for the default server), for the ``server`` argument of tmux calls
@@ -1908,12 +1942,17 @@ def _detach(args: list[str]) -> None:
1908
1942
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
1909
1943
 
1910
1944
 
1945
+ # A shell that starts the command after it in the background and exits, so the
1946
+ # command is nobody's child (see _detach_orphan).
1947
+ ORPHANED = ["/bin/sh", "-c", '"$@" </dev/null >/dev/null 2>&1 &', "sh"]
1948
+
1949
+
1911
1950
  def _detach_orphan(args: list[str]) -> None:
1912
1951
  """Start a brindle helper that outlives this process and its agent: a
1913
1952
  shell starts it in the background and exits, so it is nobody's child.
1914
1953
  Stopping an agent stops every process descended from its MCP server
1915
1954
  (brindle.procs), a helper ``_detach`` started there included."""
1916
- subprocess.run(["/bin/sh", "-c", '"$@" </dev/null >/dev/null 2>&1 &', "sh", *args],
1955
+ subprocess.run([*ORPHANED, *args],
1917
1956
  start_new_session=True, stdin=subprocess.DEVNULL,
1918
1957
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False)
1919
1958
 
@@ -2154,12 +2193,16 @@ def delegate(
2154
2193
  branch: str | None = None,
2155
2194
  done_when: str | None = None,
2156
2195
  plan_first: bool | None = None,
2196
+ start: str | None = None,
2197
+ files: list[str] | None = None,
2157
2198
  ) -> tuple[Agent, Workspace]:
2158
2199
  """Start a worker. ``plan_first`` (None: the repo's ``plan_first`` config)
2159
- makes it get a plan approved before it may edit. With ``isolate``, the worker gets a new worktree whose
2200
+ makes it get a plan approved before it may edit; ``files`` picks the
2201
+ decisions its brief carries. With ``isolate``, the worker gets a new worktree whose
2160
2202
  branch starts from the caller's current branch, so it sees the caller's
2161
- committed work, and nobody edits the same files. Refuses beyond the
2162
- repo's ``max_agents`` workers running at once."""
2203
+ committed work, and nobody edits the same files; ``start`` names another
2204
+ commit to start it from (it still merges into the caller's branch).
2205
+ Refuses beyond the repo's ``max_agents`` workers running at once."""
2163
2206
  from brindle import autopilot as pilot
2164
2207
  from brindle import repos
2165
2208
  from brindle.config import load_repo_config
@@ -2190,7 +2233,7 @@ def delegate(
2190
2233
  merge_into = load_repo_config(caller_ws.repo_root).merge_into
2191
2234
  base = merge_into if merge_into and not (caller and caller.parent_id) else caller_ws.branch
2192
2235
  branch = branch or _branch_from_task(profile, task, new_id()[:4])
2193
- start = base if git.branch_exists(caller_ws.repo_root, base) else "HEAD"
2236
+ start = start or (base if git.branch_exists(caller_ws.repo_root, base) else "HEAD")
2194
2237
  created = workspaces.create(
2195
2238
  db, caller_ws.path, branch, base, fetch=False, start=start,
2196
2239
  reuse_registered=True,
@@ -2205,7 +2248,7 @@ def delegate(
2205
2248
  # the new pane for up to 30 seconds.
2206
2249
  agent = spawn(
2207
2250
  db, ws, profile, prompt=task, parent_id=caller.id if caller else None, mode=mode,
2208
- done_when=done_when, background_setup=True, plan_first=bool(plan_first),
2251
+ done_when=done_when, background_setup=True, plan_first=bool(plan_first), files=files,
2209
2252
  )
2210
2253
  from brindle import events
2211
2254
 
@@ -2350,7 +2393,7 @@ def _live_reviewer(db: DB, ws: Workspace, sha: str, profile: str | None = None)
2350
2393
  ``profile``, only one running that profile (a second review by another
2351
2394
  profile is not a duplicate)."""
2352
2395
  for a in db.list_agents(ws.id):
2353
- if a.mode == "review" and a.result is None and is_alive(a) and a.review_sha == sha \
2396
+ if a.mode == "review" and a.result is None and alive_or_starting(a) and a.review_sha == sha \
2354
2397
  and (profile is None or a.profile == profile):
2355
2398
  return a
2356
2399
  return None
@@ -2386,7 +2429,9 @@ def start_review(db: DB, caller: Agent | None, ws: Workspace, profile: str | Non
2386
2429
  argv += ["--profile", profile]
2387
2430
  if focus:
2388
2431
  argv += ["--focus", focus]
2389
- _detach(argv)
2432
+ # Nobody's child: the worker reporting here may be closed while the checks
2433
+ # still run, and stopping it would stop this run too, so no reviewer (#190).
2434
+ _detach([*ORPHANED, *argv])
2390
2435
  return None
2391
2436
 
2392
2437
 
@@ -2417,34 +2462,36 @@ def review_after_checks(db: DB, caller_id: str | None, ws: Workspace, profile: s
2417
2462
  a new report (its worker committed again, or merged its moved base in)
2418
2463
  is checked again on its new head, up to ``MOVED_RETRIES`` times, and then
2419
2464
  goes to the supervisor. Raises ``AgentError`` when the reviewer can't
2420
- start."""
2465
+ start. Holds ``pipeline.review_run`` throughout, so the sweep can tell
2466
+ this run at work (queued for a slot, say) from one that died."""
2421
2467
  from brindle import gates, pipeline
2422
2468
  from brindle.config import load_repo_config
2423
2469
 
2424
2470
  cfg = cfg or load_repo_config(ws.repo_root)
2425
- since = pipeline.review_claim(db, ws)
2426
- for _attempt in range(MOVED_RETRIES + 1):
2427
- sha = gates.head(ws)
2428
- if _live_reviewer(db, ws, sha, profile):
2429
- return None
2430
- try:
2431
- summary = gates.check_summary(db, ws, cfg, cancel=lambda sha=sha: gates.head(ws) != sha)
2432
- except gates.Abandoned:
2433
- summary = None
2434
- except Exception as e: # noqa: BLE001 - the review goes ahead, saying so
2435
- summary = f"(running the checks crashed: {e})"
2436
- if db.get_workspace(ws.id) is None:
2437
- return None
2438
- if gates.head(ws) == sha:
2439
- break
2440
- if not pipeline.still_claimed(db, ws, since):
2471
+ with pipeline.review_run(ws.id):
2472
+ since = pipeline.review_claim(db, ws)
2473
+ for _attempt in range(MOVED_RETRIES + 1):
2474
+ sha = gates.head(ws)
2475
+ if _live_reviewer(db, ws, sha, profile):
2476
+ return None
2477
+ try:
2478
+ summary = gates.check_summary(db, ws, cfg, cancel=lambda sha=sha: gates.head(ws) != sha)
2479
+ except gates.Abandoned:
2480
+ summary = None
2481
+ except Exception as e: # noqa: BLE001 - the review goes ahead, saying so
2482
+ summary = f"(running the checks crashed: {e})"
2483
+ if db.get_workspace(ws.id) is None:
2484
+ return None
2485
+ if gates.head(ws) == sha:
2486
+ break
2487
+ if not pipeline.still_claimed(db, ws, since):
2488
+ return None
2489
+ else:
2490
+ pipeline.on_moved_during_checks(db, ws)
2441
2491
  return None
2442
- else:
2443
- pipeline.on_moved_during_checks(db, ws)
2444
- return None
2445
- caller = db.get_agent(caller_id) if caller_id else None
2446
- text = f"Checks for {ws.branch} at {sha[:8]}:\n\n{summary}" if summary else None
2447
- return request_review(db, caller, ws, profile, focus, cfg, check_summary=text)
2492
+ caller = db.get_agent(caller_id) if caller_id else None
2493
+ text = f"Checks for {ws.branch} at {sha[:8]}:\n\n{summary}" if summary else None
2494
+ return request_review(db, caller, ws, profile, focus, cfg, check_summary=text)
2448
2495
 
2449
2496
 
2450
2497
  def request_review(db: DB, caller: Agent | None, ws: Workspace, profile: str | None = None,
@@ -2469,7 +2516,7 @@ def request_review(db: DB, caller: Agent | None, ws: Workspace, profile: str | N
2469
2516
  cfg = cfg or load_repo_config(ws.repo_root)
2470
2517
  sha = gates.head(ws)
2471
2518
  for old in db.list_agents(ws.id):
2472
- if old.mode != "review" or old.result is not None or not is_alive(old):
2519
+ if old.mode != "review" or old.result is not None or not alive_or_starting(old):
2473
2520
  continue
2474
2521
  if old.unreported_noted:
2475
2522
  continue # it stopped without a verdict: not the reviewer of anything
@@ -2951,7 +2998,11 @@ def handle_hook(db: DB, agent_id: str, event: str, payload: dict) -> dict | None
2951
2998
  # Queued messages (worker results) are typed into an idle chat too;
2952
2999
  # only a prompt brindle didn't deliver is the user speaking.
2953
3000
  prompt = str(payload.get("prompt", ""))
2954
- if agent.mode == "interactive" and not db.recently_delivered(agent_id, prompt):
3001
+ # brindle's own reminders also arrive as prompts, possibly wrapped or
3002
+ # sent without being recorded as delivered (the unread-mail one): any
3003
+ # prompt carrying brindle's tag is not the user speaking.
3004
+ if (agent.mode == "interactive" and "[brindle" not in prompt
3005
+ and not db.recently_delivered(agent_id, prompt)):
2955
3006
  from brindle import autopilot as pilot
2956
3007
 
2957
3008
  pilot.user_spoke(db, agent)
@@ -110,7 +110,9 @@ def binary() -> str:
110
110
  explicit = os.environ.get("BRINDLE_AGY_BIN")
111
111
  if explicit:
112
112
  return explicit
113
- return shutil.which("agy") or (BUNDLED if os.path.exists(BUNDLED) else "agy")
113
+ from brindle.providers import which_cli
114
+
115
+ return which_cli("agy") or (BUNDLED if os.path.exists(BUNDLED) else "agy")
114
116
 
115
117
 
116
118
  def tool_names() -> list[str]: