brindle 0.1.0__tar.gz → 0.2.1__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 (117) hide show
  1. {brindle-0.1.0 → brindle-0.2.1}/.gitignore +1 -0
  2. {brindle-0.1.0 → brindle-0.2.1}/PKG-INFO +16 -11
  3. {brindle-0.1.0 → brindle-0.2.1}/README.md +15 -10
  4. {brindle-0.1.0 → brindle-0.2.1}/pyproject.toml +6 -2
  5. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/agents.py +150 -120
  6. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/autopilot.py +98 -15
  7. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/budget.py +48 -5
  8. brindle-0.2.1/src/brindle/builtin_agents/developer-codex.md +6 -0
  9. brindle-0.2.1/src/brindle/builtin_agents/developer-heavy.md +7 -0
  10. brindle-0.2.1/src/brindle/builtin_agents/developer-light.md +7 -0
  11. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/builtin_agents/developer.md +4 -3
  12. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/builtin_agents/reviewer-codex.md +4 -9
  13. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/builtin_agents/reviewer-local.md +3 -6
  14. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/builtin_agents/reviewer.md +4 -9
  15. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/ci_adapters.py +1 -1
  16. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/ci_client.py +14 -3
  17. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/cli.py +144 -24
  18. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/config.py +31 -6
  19. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/cost.py +181 -20
  20. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/cost_estimate.py +12 -7
  21. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/cull.py +9 -2
  22. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/db.py +59 -3
  23. brindle-0.2.1/src/brindle/escapes.py +176 -0
  24. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/events.py +42 -1
  25. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/gates.py +82 -20
  26. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/history.py +2 -1
  27. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/learning.py +31 -6
  28. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/mcp_server.py +105 -35
  29. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pipeline.py +41 -11
  30. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pricing.py +2 -1
  31. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/account.py +14 -3
  32. brindle-0.2.1/src/brindle/pro/brief_learning.py +164 -0
  33. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/learning.py +71 -6
  34. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/rollout.py +39 -12
  35. brindle-0.2.1/src/brindle/pro/status.py +591 -0
  36. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/team_events.py +37 -3
  37. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/team_policy.py +61 -7
  38. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/profiles.py +9 -2
  39. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/savings.py +4 -2
  40. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/scratch.py +34 -2
  41. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/tasks.py +86 -3
  42. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/tmux.py +29 -3
  43. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/usage.py +12 -6
  44. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/watch.py +83 -9
  45. brindle-0.2.1/src/brindle/watch_costs.py +303 -0
  46. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/workspaces.py +58 -0
  47. brindle-0.1.0/src/brindle/builtin_agents/developer-codex.md +0 -21
  48. brindle-0.1.0/src/brindle/builtin_agents/developer-heavy.md +0 -23
  49. {brindle-0.1.0 → brindle-0.2.1}/LICENSE +0 -0
  50. {brindle-0.1.0 → brindle-0.2.1}/SCHEDULE-A +0 -0
  51. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/__init__.py +0 -0
  52. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/__main__.py +0 -0
  53. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/account.py +0 -0
  54. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/airgap.py +0 -0
  55. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/antigravity.py +0 -0
  56. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/builtin_agents/__init__.py +0 -0
  57. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/builtin_agents/backend-auditor.md +0 -0
  58. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/builtin_agents/developer-local.md +0 -0
  59. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/builtin_agents/qa.md +0 -0
  60. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/builtin_agents/subagent.md +0 -0
  61. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/builtin_agents/supervisor.md +0 -0
  62. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/builtin_rules/__init__.py +0 -0
  63. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/builtin_rules/security/backend.md +0 -0
  64. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/builtin_rules/style/minimal-diff.md +0 -0
  65. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/builtin_rules/tests/only.md +0 -0
  66. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/ci_federation.py +0 -0
  67. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/ci_hosts.py +0 -0
  68. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/codemap.py +0 -0
  69. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/codex_hook.py +0 -0
  70. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/conflicts.py +0 -0
  71. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/demo.py +0 -0
  72. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/detect.py +0 -0
  73. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/doctor.py +0 -0
  74. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/git.py +0 -0
  75. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/guardrails.py +0 -0
  76. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/inbox.py +0 -0
  77. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/learned_rules.py +0 -0
  78. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/legacy.py +0 -0
  79. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/native/__init__.py +0 -0
  80. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/native/client.py +0 -0
  81. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/native/loop.py +0 -0
  82. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/native/permissions.py +0 -0
  83. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/native/runner.py +0 -0
  84. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/native/serve.py +0 -0
  85. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/native/tools.py +0 -0
  86. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/permissions.py +0 -0
  87. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/plugins.py +0 -0
  88. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/policy.py +0 -0
  89. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pool.py +0 -0
  90. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/__init__.py +0 -0
  91. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/_files.py +0 -0
  92. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/audit_chain.py +0 -0
  93. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/audit_export.py +0 -0
  94. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/auth.py +0 -0
  95. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/cost_centers.py +0 -0
  96. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/credentials.py +0 -0
  97. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/features.py +0 -0
  98. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/keys.py +0 -0
  99. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/license.py +0 -0
  100. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/loopback.py +0 -0
  101. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/managed_models.py +0 -0
  102. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/org_profiles.py +0 -0
  103. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/orgkey.py +0 -0
  104. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/settings_sync.py +0 -0
  105. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/procs.py +0 -0
  106. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/providers.py +0 -0
  107. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/quota.py +0 -0
  108. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/repo_cmds.py +0 -0
  109. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/repos.py +0 -0
  110. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/rewind.py +0 -0
  111. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/rule_checks.py +0 -0
  112. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/secrets.py +0 -0
  113. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/services.py +0 -0
  114. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/sessions.py +0 -0
  115. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/sidebar_follow.py +0 -0
  116. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/status_cache.py +0 -0
  117. {brindle-0.1.0 → brindle-0.2.1}/src/brindle/view.py +0 -0
@@ -9,3 +9,4 @@ graphify-out/
9
9
  # private, kept local for now
10
10
  docs/
11
11
  .brindle/config.local.json
12
+ .brindle/bench.json
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: brindle
3
- Version: 0.1.0
3
+ Version: 0.2.1
4
4
  Summary: Run coding agents in parallel, each on its own git branch, with a supervisor that delegates, reviews, and merges
5
5
  Project-URL: Homepage, https://pawdelta.com/brindle/
6
6
  Project-URL: Documentation, https://pawdelta.com/brindle/docs/recommended-use
@@ -89,7 +89,7 @@ uv tool install --editable ~/Projects/brindle # or from a local checkout
89
89
  brindle drives Claude Code, so you need that too (`npm install -g @anthropic-ai/claude-code`).
90
90
 
91
91
  `brindle --version` prints the installed version; the tmux status bar of every brindle
92
- session shows it too (`brindle 0.1.0`). A session started before an upgrade keeps
92
+ session shows it too (`brindle 0.2.1`). A session started before an upgrade keeps
93
93
  running the old code, and shows the old number, until you restart it.
94
94
 
95
95
  ## Quick start
@@ -360,6 +360,7 @@ your own status line prints, so what you see doesn't change.
360
360
  | `brindle cost requests` | Enterprise: your approval requests and their status; checks pending ones with the server (the cull pass also does, every few minutes). With cost centers, spend events also carry the repo (`owner/name`) so the org can total spend per center |
361
361
  | `brindle cost estimate [--tasks N] [--weight W] [--profile P] [--reviewer R]` | what the current goal (or N tasks) is likely to cost, as a range from this repo's history (median to p80 of past worker and review runs), plus a cheaper alternative with a different reviewer; says "low confidence" when there's too little history. Also added to `set_goal` and `assign` replies (brindle Pro) |
362
362
  | `brindle learning` | whether brindle Pro's hosted learning is on for this repo, and if not, why (nothing is learned on your machine) |
363
+ | `brindle learning seed` | Pro: sends this repo's finished routing history to the hosted learner once, so it starts with what this machine has seen. Only the usual coarse record fields leave (kind/size one-hots, counters, HMAC keys), never task text or paths. A second run sends nothing new; decisions already sent live are skipped |
363
364
  | `brindle account [login\|logout\|status\|upgrade\|portal\|org]` | paid features: bare `brindle account` shows what your plan has and how to get the rest (see "brindle Pro and Team" below) |
364
365
  | `brindle audit verify\|export\|pubkey\|ship\|prune` | the local tamper-evident audit log, and shipping it to your SIEM (brindle Enterprise; see "Audit log" below) |
365
366
  | `brindle watch [--all] [--once]` | the dashboard on its own (the same view as the sidebar): enter attaches, `p` peeks, `x` closes |
@@ -389,12 +390,13 @@ knowing them helps when you tell the supervisor how to work.
389
390
 
390
391
  | Tool | Used by | |
391
392
  |---|---|---|
392
- | `assign` / `handoff` / `wait_for_worker` | supervisor | start a worker (return now / wait for its result / keep waiting) |
393
+ | `assign` / `handoff` / `wait_for_worker` | supervisor | start a worker (return now / wait for its result / keep waiting); `dry_run=true` on `assign`/`handoff` starts nothing and reports the profile, what each `files` glob matches, overlaps and whether it would queue |
393
394
  | `send_message` | any agent | message another agent; delivered when it's idle |
394
395
  | `read_messages` | supervisor | read the messages agents and brindle sent you and mark them read; with `message_delivery` `"pull"` (the default) you get a one-line "N new messages" notice instead of each message |
395
- | `list_agents` / `list_tasks` / `list_agent_profiles` | supervisor | who's running, what's queued, which profiles exist |
396
+ | `list_agents` / `list_tasks` / `list_agent_profiles` | supervisor | who's running, what's queued, which profiles exist; `list_tasks(full=true)` also shows each task's brief and `done_when` |
396
397
  | `list_repos` | supervisor | the repos attached to the session (brindle Pro); `repo=<alias>` on `assign`/`handoff` puts a worker there |
397
398
  | `cancel_task` | supervisor | cancel a queued task (and its dependents) to re-plan |
399
+ | `requeue` | supervisor | queue a cancelled task again with the same brief, files and profile; `depends_on` replaces its dependencies |
398
400
  | `workspace_diff` | supervisor | a worker branch's changes against its base |
399
401
  | `request_review` / `submit_review` | supervisor / reviewer | start a reviewer on a branch / record its verdict |
400
402
  | `merge_workspace` / `remove_workspace` | supervisor | merge through the gates / delete the worktree |
@@ -437,8 +439,10 @@ count as unknown.
437
439
  `brindle cost` prices the last 30 days of history at each model's list price
438
440
  (per million tokens: input, output, cache write, cache read), copied from
439
441
  Anthropic's and OpenAI's pricing pages and dated; brindle warns when they're
440
- more than 90 days old. Local models cost $0. A model brindle has no price
441
- for is shown as unpriced tokens, never guessed. Add or correct prices under
442
+ more than 90 days old. Local models cost $0, and usage on a model known to be
443
+ free reads "Free" (its tokens still count; a mix reads "$1.20 (+40k tokens
444
+ free)"). A model brindle has no price for is shown as unpriced tokens, never
445
+ guessed. Add or correct prices under
442
446
  `pricing` in `~/.brindle/config.json` or `.brindle/config.json`:
443
447
 
444
448
  ```json
@@ -538,7 +542,7 @@ Autopilot, merge gates and cleanup:
538
542
  | `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") |
539
543
  | `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 |
540
544
  | `add_dirs` | `[]` | directories outside the worktree that Claude Code agents may use (`--add-dir`; full tool access, see "Directories outside the workspace") |
541
- | `local_models` | `false` | `true`: when a native profile points at Ollama on this machine and it isn't running, `brindle` starts `ollama serve` in the background (with the context length the profiles need) and loads their models. Off, brindle uses a server that's already running and never starts or preloads one |
545
+ | `local_models` | `true` | When a native profile points at Ollama on this machine and it isn't running, `brindle` starts `ollama serve` in the background (with the context length the profiles need) and loads their models, then stops it when the last session closes. `false`: brindle uses a server that's already running and never starts or preloads one |
542
546
  | `sidebar` | `"left"` | where the dashboard sits in each window: `"left"` of the chat, or `"bottom"` (full-width rows under it) |
543
547
  | `delegation` | `"balanced"` | how readily the supervisor hands work to workers. `"conservative"` does most work in its own chat (fewest tokens), `"fast"` splits any multi-part request across parallel workers straight away (quickest, most tokens). `brindle delegation fast` saves it for every repo and session (in `~/.brindle/config.json`; `--repo` for this repo only) and tells a running supervisor |
544
548
  | `delete_merged_branches` | `true` | removing a worktree (after a merge, `brindle rm`, `brindle prune`, session cleanup) also deletes its branch once every commit is in its base, so finished branches don't pile up. An unmerged branch is always kept; `false` keeps them all. If GitHub keeps merged PR branches, the first `brindle pr` in a repo offers to turn on its automatic deletion with your `gh` login (repo admins only) |
@@ -742,10 +746,11 @@ how to use them, and the next step to get the rest:
742
746
  | Brindle-CI (fixes builds, issues into verified pull requests, validation) | Team | `brindle ci init` |
743
747
  | org profile and rule-pack library, pinned so a repo can't loosen it | Team | `brindle account org profiles` |
744
748
  | org budgets and protected paths | Team | `budget`, `protected_paths` in your org policy (`brindle account org policy`) |
749
+ | per-member budgets, pause, and admin messages (sidebar "Messages", the bottom bar, `brindle org messages`) | Team | `brindle org messages [--keep]` lists and marks read the notices your admins sent you; a pause refuses new workers and stops running ones |
745
750
  | audit log, air-gap | Enterprise | `brindle audit verify`, `"airgap": true` |
746
751
  | audit export | Enterprise | `"audit_export"` in `~/.brindle/config.json`, `brindle audit ship` |
747
752
  | managed models | Enterprise | `provider_config`, `deny_personal_keys` in your org policy (see "Managed models" below) |
748
- | wider CI, cost centers, managed rollout | Enterprise | `brindle ci init --host gitlab`, `brindle cost request`, and `min_version`, required packs and the kill switch in your org policy |
753
+ | wider CI, cost centers, managed rollout | Enterprise | `brindle ci init --host gitlab`, `brindle cost request`, and `min_version`, required packs and the kill switch in your org policy; admins can also shut down or throttle a member, role or org remotely (it arrives within about a minute) |
749
754
 
750
755
  `brindle ci init` checks your plan before it changes anything: it refuses
751
756
  without Brindle-CI on the org you're using, and on Pro it sets up only the fix
@@ -1548,9 +1553,9 @@ ollama pull qwen3-coder:30b
1548
1553
  brindle doctor # "model qwen3-coder:30b ... is available"
1549
1554
  ```
1550
1555
 
1551
- Run `ollama serve` yourself, or let brindle do it: a 30B model holds about 20 GB
1552
- of GPU memory, so brindle doesn't start or preload one unless you set
1553
- `"local_models": true`. Then, when `brindle` (or `brindle continue`)
1556
+ Run `ollama serve` yourself, or let brindle do it, with no setup. A 30B model holds
1557
+ about 20 GB of GPU memory, so set `"local_models": false` if you don't want brindle
1558
+ to start or preload one. When `brindle` (or `brindle continue`)
1554
1559
  starts and a native profile points at Ollama on this machine that isn't answering,
1555
1560
  brindle starts it in the background with `OLLAMA_CONTEXT_LENGTH` set to the largest
1556
1561
  `context_tokens` any profile asks of it plus room for the reply (40960 for the
@@ -59,7 +59,7 @@ uv tool install --editable ~/Projects/brindle # or from a local checkout
59
59
  brindle drives Claude Code, so you need that too (`npm install -g @anthropic-ai/claude-code`).
60
60
 
61
61
  `brindle --version` prints the installed version; the tmux status bar of every brindle
62
- session shows it too (`brindle 0.1.0`). A session started before an upgrade keeps
62
+ session shows it too (`brindle 0.2.1`). A session started before an upgrade keeps
63
63
  running the old code, and shows the old number, until you restart it.
64
64
 
65
65
  ## Quick start
@@ -330,6 +330,7 @@ your own status line prints, so what you see doesn't change.
330
330
  | `brindle cost requests` | Enterprise: your approval requests and their status; checks pending ones with the server (the cull pass also does, every few minutes). With cost centers, spend events also carry the repo (`owner/name`) so the org can total spend per center |
331
331
  | `brindle cost estimate [--tasks N] [--weight W] [--profile P] [--reviewer R]` | what the current goal (or N tasks) is likely to cost, as a range from this repo's history (median to p80 of past worker and review runs), plus a cheaper alternative with a different reviewer; says "low confidence" when there's too little history. Also added to `set_goal` and `assign` replies (brindle Pro) |
332
332
  | `brindle learning` | whether brindle Pro's hosted learning is on for this repo, and if not, why (nothing is learned on your machine) |
333
+ | `brindle learning seed` | Pro: sends this repo's finished routing history to the hosted learner once, so it starts with what this machine has seen. Only the usual coarse record fields leave (kind/size one-hots, counters, HMAC keys), never task text or paths. A second run sends nothing new; decisions already sent live are skipped |
333
334
  | `brindle account [login\|logout\|status\|upgrade\|portal\|org]` | paid features: bare `brindle account` shows what your plan has and how to get the rest (see "brindle Pro and Team" below) |
334
335
  | `brindle audit verify\|export\|pubkey\|ship\|prune` | the local tamper-evident audit log, and shipping it to your SIEM (brindle Enterprise; see "Audit log" below) |
335
336
  | `brindle watch [--all] [--once]` | the dashboard on its own (the same view as the sidebar): enter attaches, `p` peeks, `x` closes |
@@ -359,12 +360,13 @@ knowing them helps when you tell the supervisor how to work.
359
360
 
360
361
  | Tool | Used by | |
361
362
  |---|---|---|
362
- | `assign` / `handoff` / `wait_for_worker` | supervisor | start a worker (return now / wait for its result / keep waiting) |
363
+ | `assign` / `handoff` / `wait_for_worker` | supervisor | start a worker (return now / wait for its result / keep waiting); `dry_run=true` on `assign`/`handoff` starts nothing and reports the profile, what each `files` glob matches, overlaps and whether it would queue |
363
364
  | `send_message` | any agent | message another agent; delivered when it's idle |
364
365
  | `read_messages` | supervisor | read the messages agents and brindle sent you and mark them read; with `message_delivery` `"pull"` (the default) you get a one-line "N new messages" notice instead of each message |
365
- | `list_agents` / `list_tasks` / `list_agent_profiles` | supervisor | who's running, what's queued, which profiles exist |
366
+ | `list_agents` / `list_tasks` / `list_agent_profiles` | supervisor | who's running, what's queued, which profiles exist; `list_tasks(full=true)` also shows each task's brief and `done_when` |
366
367
  | `list_repos` | supervisor | the repos attached to the session (brindle Pro); `repo=<alias>` on `assign`/`handoff` puts a worker there |
367
368
  | `cancel_task` | supervisor | cancel a queued task (and its dependents) to re-plan |
369
+ | `requeue` | supervisor | queue a cancelled task again with the same brief, files and profile; `depends_on` replaces its dependencies |
368
370
  | `workspace_diff` | supervisor | a worker branch's changes against its base |
369
371
  | `request_review` / `submit_review` | supervisor / reviewer | start a reviewer on a branch / record its verdict |
370
372
  | `merge_workspace` / `remove_workspace` | supervisor | merge through the gates / delete the worktree |
@@ -407,8 +409,10 @@ count as unknown.
407
409
  `brindle cost` prices the last 30 days of history at each model's list price
408
410
  (per million tokens: input, output, cache write, cache read), copied from
409
411
  Anthropic's and OpenAI's pricing pages and dated; brindle warns when they're
410
- more than 90 days old. Local models cost $0. A model brindle has no price
411
- for is shown as unpriced tokens, never guessed. Add or correct prices under
412
+ more than 90 days old. Local models cost $0, and usage on a model known to be
413
+ free reads "Free" (its tokens still count; a mix reads "$1.20 (+40k tokens
414
+ free)"). A model brindle has no price for is shown as unpriced tokens, never
415
+ guessed. Add or correct prices under
412
416
  `pricing` in `~/.brindle/config.json` or `.brindle/config.json`:
413
417
 
414
418
  ```json
@@ -508,7 +512,7 @@ Autopilot, merge gates and cleanup:
508
512
  | `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") |
509
513
  | `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 |
510
514
  | `add_dirs` | `[]` | directories outside the worktree that Claude Code agents may use (`--add-dir`; full tool access, see "Directories outside the workspace") |
511
- | `local_models` | `false` | `true`: when a native profile points at Ollama on this machine and it isn't running, `brindle` starts `ollama serve` in the background (with the context length the profiles need) and loads their models. Off, brindle uses a server that's already running and never starts or preloads one |
515
+ | `local_models` | `true` | When a native profile points at Ollama on this machine and it isn't running, `brindle` starts `ollama serve` in the background (with the context length the profiles need) and loads their models, then stops it when the last session closes. `false`: brindle uses a server that's already running and never starts or preloads one |
512
516
  | `sidebar` | `"left"` | where the dashboard sits in each window: `"left"` of the chat, or `"bottom"` (full-width rows under it) |
513
517
  | `delegation` | `"balanced"` | how readily the supervisor hands work to workers. `"conservative"` does most work in its own chat (fewest tokens), `"fast"` splits any multi-part request across parallel workers straight away (quickest, most tokens). `brindle delegation fast` saves it for every repo and session (in `~/.brindle/config.json`; `--repo` for this repo only) and tells a running supervisor |
514
518
  | `delete_merged_branches` | `true` | removing a worktree (after a merge, `brindle rm`, `brindle prune`, session cleanup) also deletes its branch once every commit is in its base, so finished branches don't pile up. An unmerged branch is always kept; `false` keeps them all. If GitHub keeps merged PR branches, the first `brindle pr` in a repo offers to turn on its automatic deletion with your `gh` login (repo admins only) |
@@ -712,10 +716,11 @@ how to use them, and the next step to get the rest:
712
716
  | Brindle-CI (fixes builds, issues into verified pull requests, validation) | Team | `brindle ci init` |
713
717
  | org profile and rule-pack library, pinned so a repo can't loosen it | Team | `brindle account org profiles` |
714
718
  | org budgets and protected paths | Team | `budget`, `protected_paths` in your org policy (`brindle account org policy`) |
719
+ | per-member budgets, pause, and admin messages (sidebar "Messages", the bottom bar, `brindle org messages`) | Team | `brindle org messages [--keep]` lists and marks read the notices your admins sent you; a pause refuses new workers and stops running ones |
715
720
  | audit log, air-gap | Enterprise | `brindle audit verify`, `"airgap": true` |
716
721
  | audit export | Enterprise | `"audit_export"` in `~/.brindle/config.json`, `brindle audit ship` |
717
722
  | managed models | Enterprise | `provider_config`, `deny_personal_keys` in your org policy (see "Managed models" below) |
718
- | wider CI, cost centers, managed rollout | Enterprise | `brindle ci init --host gitlab`, `brindle cost request`, and `min_version`, required packs and the kill switch in your org policy |
723
+ | wider CI, cost centers, managed rollout | Enterprise | `brindle ci init --host gitlab`, `brindle cost request`, and `min_version`, required packs and the kill switch in your org policy; admins can also shut down or throttle a member, role or org remotely (it arrives within about a minute) |
719
724
 
720
725
  `brindle ci init` checks your plan before it changes anything: it refuses
721
726
  without Brindle-CI on the org you're using, and on Pro it sets up only the fix
@@ -1518,9 +1523,9 @@ ollama pull qwen3-coder:30b
1518
1523
  brindle doctor # "model qwen3-coder:30b ... is available"
1519
1524
  ```
1520
1525
 
1521
- Run `ollama serve` yourself, or let brindle do it: a 30B model holds about 20 GB
1522
- of GPU memory, so brindle doesn't start or preload one unless you set
1523
- `"local_models": true`. Then, when `brindle` (or `brindle continue`)
1526
+ Run `ollama serve` yourself, or let brindle do it, with no setup. A 30B model holds
1527
+ about 20 GB of GPU memory, so set `"local_models": false` if you don't want brindle
1528
+ to start or preload one. When `brindle` (or `brindle continue`)
1524
1529
  starts and a native profile points at Ollama on this machine that isn't answering,
1525
1530
  brindle starts it in the background with `OLLAMA_CONTEXT_LENGTH` set to the largest
1526
1531
  `context_tokens` any profile asks of it plus room for the reply (40960 for the
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "brindle"
3
- version = "0.1.0"
3
+ version = "0.2.1"
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"
@@ -49,7 +49,10 @@ audit = "brindle.pro.audit_chain:make"
49
49
  audit_export = "brindle.pro.audit_export:make"
50
50
 
51
51
  [dependency-groups]
52
- dev = ["pytest>=8"]
52
+ dev = [
53
+ "pytest>=8",
54
+ "pytest-xdist>=3.8.0",
55
+ ]
53
56
 
54
57
  [build-system]
55
58
  requires = ["hatchling>=1.27"]
@@ -69,3 +72,4 @@ only-include = ["src", "README.md", "LICENSE", "SCHEDULE-A", "pyproject.toml"]
69
72
  [tool.pytest.ini_options]
70
73
  testpaths = ["tests"]
71
74
  pythonpath = ["tests"]
75
+ markers = ["serial: run outside xdist (`pytest -m serial`); the parallel pass skips these"]
@@ -47,6 +47,12 @@ WORKER_FOOTER = """
47
47
  ---
48
48
  You are running as a brindle worker (agent id {agent_id}) on branch `{branch}`.
49
49
  {guidance}
50
+ Before you finish, check your work against the task and its finish line, item
51
+ by item: each behaviour it asks for needs a test that would fail without your
52
+ change, and a requirement with no such test isn't done yet. If the task turns
53
+ out too big for one branch, finish a coherent part and say exactly what's left
54
+ (and how you'd split it) rather than leaving every part half done.
55
+
50
56
  When you have finished:
51
57
  1. Commit your work to this branch with a clear message (do not push or merge).
52
58
  2. Call the `report_result` tool from the `brindle` MCP server with a concise
@@ -72,6 +78,12 @@ Your shell may start in another directory, so begin every Bash command with
72
78
  `cd {path} && ` (or use `git -C {path}`), and give file tools absolute paths
73
79
  under that directory. Don't change files anywhere else, and don't switch branches.
74
80
  {guidance}
81
+ Before you finish, check your work against the task and its finish line, item
82
+ by item: each behaviour it asks for needs a test that would fail without your
83
+ change, and a requirement with no such test isn't done yet. If the task turns
84
+ out too big for one branch, finish a coherent part and say exactly what's left
85
+ (and how you'd split it) rather than leaving every part half done.
86
+
75
87
  When you have finished:
76
88
  1. Commit your work there on `{branch}` with a clear message (do not push or merge).
77
89
  2. Don't call any brindle tools. End with a concise summary: what you changed,
@@ -1358,11 +1370,16 @@ def report_result(db: DB, agent_id: str, result: str, forward: bool = True,
1358
1370
  agent=agent, usage=u, branch=ws.branch, task=agent.task, result=result,
1359
1371
  )
1360
1372
  if u and u.total:
1373
+ free = False
1361
1374
  try:
1362
- dollars = usage_mod.usage_cost(u, pricing.repo_overrides(ws.repo_root if ws else None))
1375
+ root = ws.repo_root if ws else None
1376
+ extra = pricing.repo_overrides(root)
1377
+ price = pricing.price_for(u.model, extra) or pricing.profile_price(agent.profile, root, extra)
1378
+ dollars = usage_mod.usage_cost(u, extra, fallback=price)
1379
+ free = price == pricing.FREE
1363
1380
  except Exception: # noqa: BLE001 - a price is an extra too
1364
1381
  dollars = None
1365
- forwarded = f"{result}\n\n{usage_mod.summary_line(u, dollars)}"
1382
+ forwarded = f"{result}\n\n{usage_mod.summary_line(u, dollars, free)}"
1366
1383
  else:
1367
1384
  forwarded = result
1368
1385
  if ws and agent.mode in ("handoff", "handoff_detached", "assign"):
@@ -1609,6 +1626,7 @@ def delegate(
1609
1626
  start = base if git.branch_exists(caller_ws.repo_root, base) else "HEAD"
1610
1627
  created = workspaces.create(
1611
1628
  db, caller_ws.path, branch, base, fetch=False, start=start,
1629
+ reuse_registered=True,
1612
1630
  )
1613
1631
  if created.setup and not created.setup.ok:
1614
1632
  raise AgentError(f"workspace setup failed:\n{created.setup.log}")
@@ -1691,38 +1709,19 @@ def _local_reviewer_available() -> bool:
1691
1709
  return bool(ok) and "is available" in detail
1692
1710
 
1693
1711
 
1694
- def request_review(db: DB, caller: Agent | None, ws: Workspace, profile: str | None = None,
1695
- focus: str | None = None, cfg: RepoConfig | None = None) -> Agent:
1696
- """Start a reviewer in a worker's workspace right away. Its verdict is
1697
- recorded for the merge gate and forwarded to ``caller`` as a message.
1698
- ``profile`` picks the reviewer profile; None uses
1699
- ``default_review_profile``.
1700
-
1701
- Raises ``AgentError`` if the chosen profile (explicit or picked) doesn't
1702
- exist, or if it uses the codex provider but codex isn't on PATH -- rather
1703
- than spawning a reviewer doomed to fail in a dead pane.
1704
-
1705
- ``cfg.checks`` are NOT run here (that would block the caller on the full
1706
- suite): the caller runs them in the background and delivers a pass/fail
1707
- summary to the reviewer as a message once they finish, via
1708
- ``deliver_check_summary``. If the workspace already has a review at an
1709
- earlier commit, the reviewer is pointed at just what changed since then,
1710
- when that's a plain diff (see ``is_linear_since``)."""
1711
- from brindle import gates
1712
- from brindle.config import load_repo_config
1712
+ def _review_kill_switch(ws: Workspace) -> None:
1713
1713
  from brindle.pro import rollout
1714
1714
 
1715
1715
  why_not = rollout.kill_switch_reason(ws.repo_root) # Enterprise managed rollout
1716
1716
  if why_not:
1717
1717
  raise AgentError(f"No reviewer started: {why_not}")
1718
- cfg = cfg or load_repo_config(ws.repo_root)
1719
- sha = gates.head(ws)
1720
- for old in db.list_agents(ws.id):
1721
- if old.mode != "review" or old.result is not None or not is_alive(old):
1722
- continue
1723
- if old.review_sha == sha:
1724
- return old # one reviewer per workspace and commit
1725
- close(db, old.id) # a new commit replaces the reviewer of the old one
1718
+
1719
+
1720
+ def _review_profile(db: DB, ws: Workspace, profile: str | None, cfg: RepoConfig) -> str:
1721
+ """The reviewer profile to use, checked to be one that can run: raises
1722
+ ``AgentError`` if it (explicit or picked) doesn't exist, or uses the codex
1723
+ provider but codex isn't on PATH -- rather than spawning a reviewer doomed
1724
+ to fail in a dead pane."""
1726
1725
  worker = workspace_worker(db, ws)
1727
1726
  profile = profile or default_review_profile(cfg, worker, db, ws.repo_root,
1728
1727
  worker.task if worker else None)
@@ -1737,6 +1736,125 @@ def request_review(db: DB, caller: Agent | None, ws: Workspace, profile: str | N
1737
1736
  f"reviewer profile {profile!r} can't run: {why}. "
1738
1737
  "Fix that, or set review_profile (or pass profile) to a different reviewer"
1739
1738
  )
1739
+ return profile
1740
+
1741
+
1742
+ def _live_reviewer(db: DB, ws: Workspace, sha: str, profile: str | None = None) -> Agent | None:
1743
+ """The reviewer still at work on ``ws`` at ``sha``, if any. With
1744
+ ``profile``, only one running that profile (a second review by another
1745
+ profile is not a duplicate)."""
1746
+ for a in db.list_agents(ws.id):
1747
+ if a.mode == "review" and a.result is None and is_alive(a) and a.review_sha == sha \
1748
+ and (profile is None or a.profile == profile):
1749
+ return a
1750
+ return None
1751
+
1752
+
1753
+ def start_review(db: DB, caller: Agent | None, ws: Workspace, profile: str | None = None,
1754
+ focus: str | None = None, cfg: RepoConfig | None = None) -> Agent | None:
1755
+ """Have ``ws`` reviewed. Without checks configured the reviewer starts
1756
+ right away and is returned. With checks, nothing starts yet: a detached
1757
+ ``_review-after-checks`` process runs them and starts the reviewer with
1758
+ their results in its prompt (see ``review_after_checks``), so this returns
1759
+ at once with None, however long the suite takes. Raises ``AgentError``
1760
+ for a reviewer that couldn't start whatever the checks say (the kill
1761
+ switch, an unusable profile); a failure after that reaches ``caller``
1762
+ as a message."""
1763
+ from brindle import gates
1764
+ from brindle.config import load_repo_config
1765
+ from brindle.providers import brindle_invocation
1766
+
1767
+ cfg = cfg or load_repo_config(ws.repo_root)
1768
+ if not cfg.checks:
1769
+ return request_review(db, caller, ws, profile, focus, cfg)
1770
+ _review_kill_switch(ws)
1771
+ _review_profile(db, ws, profile, cfg)
1772
+ existing = _live_reviewer(db, ws, gates.head(ws), profile)
1773
+ if existing:
1774
+ return existing # one reviewer per workspace and commit
1775
+ argv = [*brindle_invocation(), "_review-after-checks", ws.id]
1776
+ if caller:
1777
+ argv += ["--caller", caller.id]
1778
+ if profile:
1779
+ argv += ["--profile", profile]
1780
+ if focus:
1781
+ argv += ["--focus", focus]
1782
+ _detach(argv)
1783
+ return None
1784
+
1785
+
1786
+ def expected_check_wait(db: DB, ws: Workspace, cfg: RepoConfig) -> float | None:
1787
+ """Seconds the repo's checks usually take (the sum of each one's last
1788
+ passing duration), or None when any of them has never passed."""
1789
+ total = 0.0
1790
+ for cmd in cfg.checks:
1791
+ took = db.check_duration(ws.repo_root, cmd)
1792
+ if took is None:
1793
+ return None
1794
+ total += took
1795
+ return total if cfg.checks else None
1796
+
1797
+
1798
+ def review_after_checks(db: DB, caller_id: str | None, ws: Workspace, profile: str | None = None,
1799
+ focus: str | None = None, cfg: RepoConfig | None = None) -> Agent | None:
1800
+ """Run ``cfg.checks`` for ``ws`` (through ``gates.check_summary``: its
1801
+ lock and the by-sha cache, so a result a worker's report already warmed
1802
+ is instant), then start the reviewer with the results in its prompt.
1803
+ Meant to run from a detached process, which outlives the call that
1804
+ started it. Gives up quietly when the branch moves on (the new commit
1805
+ gets its own review), the workspace is gone, or a reviewer is already at
1806
+ work on this commit. Raises ``AgentError`` when the reviewer can't
1807
+ start."""
1808
+ from brindle import gates
1809
+ from brindle.config import load_repo_config
1810
+
1811
+ cfg = cfg or load_repo_config(ws.repo_root)
1812
+ sha = gates.head(ws)
1813
+ if _live_reviewer(db, ws, sha, profile):
1814
+ return None
1815
+ try:
1816
+ summary = gates.check_summary(db, ws, cfg, cancel=lambda: gates.head(ws) != sha)
1817
+ except gates.Abandoned:
1818
+ return None
1819
+ except Exception as e: # noqa: BLE001 - the review goes ahead, saying so
1820
+ summary = f"(running the checks crashed: {e})"
1821
+ if db.get_workspace(ws.id) is None or gates.head(ws) != sha:
1822
+ return None
1823
+ caller = db.get_agent(caller_id) if caller_id else None
1824
+ text = f"Checks for {ws.branch} at {sha[:8]}:\n\n{summary}" if summary else None
1825
+ return request_review(db, caller, ws, profile, focus, cfg, check_summary=text)
1826
+
1827
+
1828
+ def request_review(db: DB, caller: Agent | None, ws: Workspace, profile: str | None = None,
1829
+ focus: str | None = None, cfg: RepoConfig | None = None,
1830
+ check_summary: str | None = None) -> Agent:
1831
+ """Start a reviewer in a worker's workspace right away. Its verdict is
1832
+ recorded for the merge gate and forwarded to ``caller`` as a message.
1833
+ ``profile`` picks the reviewer profile; None uses
1834
+ ``default_review_profile`` (``AgentError`` if it can't run).
1835
+
1836
+ ``cfg.checks`` are NOT run here (that would block the caller on the full
1837
+ suite): ``start_review`` runs them first, in the background, and passes
1838
+ their results as ``check_summary`` for the reviewer's prompt. If the
1839
+ workspace already has a review at an earlier commit, the reviewer is
1840
+ pointed at just what changed since then, when that's a plain diff (see
1841
+ ``is_linear_since``)."""
1842
+ from brindle import gates
1843
+ from brindle.config import load_repo_config
1844
+
1845
+ _review_kill_switch(ws)
1846
+ cfg = cfg or load_repo_config(ws.repo_root)
1847
+ sha = gates.head(ws)
1848
+ for old in db.list_agents(ws.id):
1849
+ if old.mode != "review" or old.result is not None or not is_alive(old):
1850
+ continue
1851
+ if old.review_sha == sha:
1852
+ if profile and old.profile != profile:
1853
+ continue # another profile's review of this commit (a second review): keep both
1854
+ return old # one reviewer per workspace and commit
1855
+ close(db, old.id) # a new commit replaces the reviewer of the old one
1856
+ worker = workspace_worker(db, ws)
1857
+ profile = _review_profile(db, ws, profile, cfg)
1740
1858
 
1741
1859
  base = ws.base_branch or "the base branch"
1742
1860
  task = (f"Review the changes on branch `{ws.branch}` (workspace {ws.id}) against `{base}`: "
@@ -1749,16 +1867,9 @@ def request_review(db: DB, caller: Agent | None, ws: Workspace, profile: str | N
1749
1867
  if worker and worker.done_when:
1750
1868
  task += f"\n\nIts finish line: {worker.done_when.strip()}"
1751
1869
 
1752
- if cfg and cfg.checks:
1753
- task += ("\n\nThe repo's checks are still running (or queued behind other branches' "
1754
- "runs): you don't have their results yet. A pass/fail summary will arrive as a "
1755
- "message. Review the diff meanwhile, and don't call submit_review until you've "
1756
- "received it. If about 10 minutes pass after you've finished reviewing with no "
1757
- "such message, submit anyway and say plainly in your summary that the checks "
1758
- "were still running when you submitted, so your verdict rests on the diff "
1759
- "alone: the merge gate runs them before anything merges, and a failure that "
1760
- "arrives after your verdict goes to the supervisor. Don't run the whole suite "
1761
- "yourself to compensate.")
1870
+ if check_summary:
1871
+ task += (f"\n\nThe repo's checks have already run on this commit:\n\n{check_summary}\n\n"
1872
+ "Take these results into account; don't run the whole suite yourself.")
1762
1873
 
1763
1874
  prev = db.last_review(ws.id)
1764
1875
  if prev and prev.sha != sha:
@@ -1778,87 +1889,6 @@ def request_review(db: DB, caller: Agent | None, ws: Workspace, profile: str | N
1778
1889
  mode="review", background_setup=True, review_sha=sha)
1779
1890
 
1780
1891
 
1781
- def deliver_check_summary(db: DB, reviewer_id: str, ws: Workspace, cfg: RepoConfig) -> None:
1782
- """Run ``cfg.checks`` for ``ws`` and deliver a pass/fail summary to the
1783
- reviewer's inbox: delivered right away if it's idle, or handed over at its
1784
- next Stop, exactly like any other queued message (see ``send_message``).
1785
- Meant to run from a detached process started by request_review, so it
1786
- outlives the MCP server call that kicked it off. Always delivers
1787
- something, even if a check crashes, since the reviewer was told to wait
1788
- for this before approving. If the reviewer is no longer there to receive
1789
- it by the time the checks finish, a failing summary isn't dropped: see
1790
- ``_late_check_summary``."""
1791
- from brindle import gates
1792
-
1793
- reviewer = db.get_agent(reviewer_id)
1794
- if reviewer is None:
1795
- return
1796
- supervisor_id = reviewer.parent_id
1797
- sha = gates.head(ws) # label with the commit the checks ran on
1798
- try:
1799
- summary = gates.check_summary(db, ws, cfg)
1800
- failed = gates.summary_failed(summary)
1801
- except Exception as e:
1802
- summary, failed = f"(running the checks crashed: {e})", True
1803
-
1804
- reviewer = db.get_agent(reviewer_id)
1805
- if reviewer is None or reviewer.result is not None or reviewer.status == "done":
1806
- # It submitted its review, was closed, or was removed while the checks ran.
1807
- if failed:
1808
- _late_check_summary(db, ws, sha, summary, reviewer_id, supervisor_id)
1809
- return
1810
-
1811
- text = (f"Checks for {ws.branch} at {sha[:8]}:\n\n{summary}" if summary
1812
- else "No checks are configured for this repo.")
1813
- db.enqueue(reviewer_id, text, None)
1814
- flush(db, reviewer_id)
1815
-
1816
-
1817
- def _late_check_summary(db: DB, ws: Workspace, sha: str, summary: str, reviewer_id: str,
1818
- supervisor_id: str | None) -> None:
1819
- """The checks for ``ws`` at ``sha`` finished after its reviewer was done,
1820
- and failed (or crashed), so the verdict was given without them (a late
1821
- pass changes nothing). The failure must reach someone who can act on it:
1822
- the supervisor when the branch was approved (or has no verdict at this
1823
- commit), the worker when changes were requested anyway (the supervisor
1824
- if it's gone). Dropped
1825
- only when there's nothing left to act on: the workspace was removed, the
1826
- branch merged at this commit (its worktree may be going away under the
1827
- run), or the branch has new commits, which get their own run."""
1828
- from brindle import gates
1829
-
1830
- try:
1831
- if db.get_workspace(ws.id) is None or db.merged_sha(ws.id) == sha or gates.head(ws) != sha:
1832
- return
1833
- except git.GitError:
1834
- return
1835
- worker = workspace_worker(db, ws)
1836
- supervisor_id = supervisor_id or (worker.parent_id if worker else None)
1837
- review = db.latest_review(ws.id, sha)
1838
- checks = f"Checks for {ws.branch} at {sha[:8]}:\n\n{summary}"
1839
- if review is not None and not review.approved and worker and is_alive(worker):
1840
- try:
1841
- send_message(db, worker.id,
1842
- "The repo's checks finished after the review of your branch, and "
1843
- f"failed. Fix this along with the review's findings.\n\n{checks}")
1844
- return
1845
- except (AgentError, tmux.TmuxError):
1846
- pass # the supervisor hears instead
1847
- if supervisor_id is None or db.get_agent(supervisor_id) is None:
1848
- return
1849
- if review is not None and review.approved:
1850
- lead = (f"[brindle] The checks on `{ws.branch}` (workspace {ws.id}) FAILED, and the result "
1851
- f"arrived after reviewer {reviewer_id} had approved it: the approval rests on "
1852
- "the diff alone. The merge gate runs the checks again, so the branch won't "
1853
- "merge while they fail. Send this to the worker to fix, then have it reviewed "
1854
- "again.")
1855
- else:
1856
- lead = (f"[brindle] The checks on `{ws.branch}` (workspace {ws.id}) FAILED, and the result "
1857
- f"arrived after reviewer {reviewer_id} had finished, so the review didn't "
1858
- "take it into account. Send this to the worker to fix.")
1859
- gates.tell(db, supervisor_id, f"{lead}\n\n{checks}")
1860
-
1861
-
1862
1892
  # -- hook entry point --------------------------------------------------------
1863
1893
 
1864
1894