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.
- {brindle-0.1.0 → brindle-0.2.1}/.gitignore +1 -0
- {brindle-0.1.0 → brindle-0.2.1}/PKG-INFO +16 -11
- {brindle-0.1.0 → brindle-0.2.1}/README.md +15 -10
- {brindle-0.1.0 → brindle-0.2.1}/pyproject.toml +6 -2
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/agents.py +150 -120
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/autopilot.py +98 -15
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/budget.py +48 -5
- brindle-0.2.1/src/brindle/builtin_agents/developer-codex.md +6 -0
- brindle-0.2.1/src/brindle/builtin_agents/developer-heavy.md +7 -0
- brindle-0.2.1/src/brindle/builtin_agents/developer-light.md +7 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/builtin_agents/developer.md +4 -3
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/builtin_agents/reviewer-codex.md +4 -9
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/builtin_agents/reviewer-local.md +3 -6
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/builtin_agents/reviewer.md +4 -9
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/ci_adapters.py +1 -1
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/ci_client.py +14 -3
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/cli.py +144 -24
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/config.py +31 -6
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/cost.py +181 -20
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/cost_estimate.py +12 -7
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/cull.py +9 -2
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/db.py +59 -3
- brindle-0.2.1/src/brindle/escapes.py +176 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/events.py +42 -1
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/gates.py +82 -20
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/history.py +2 -1
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/learning.py +31 -6
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/mcp_server.py +105 -35
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pipeline.py +41 -11
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pricing.py +2 -1
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/account.py +14 -3
- brindle-0.2.1/src/brindle/pro/brief_learning.py +164 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/learning.py +71 -6
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/rollout.py +39 -12
- brindle-0.2.1/src/brindle/pro/status.py +591 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/team_events.py +37 -3
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/team_policy.py +61 -7
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/profiles.py +9 -2
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/savings.py +4 -2
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/scratch.py +34 -2
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/tasks.py +86 -3
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/tmux.py +29 -3
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/usage.py +12 -6
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/watch.py +83 -9
- brindle-0.2.1/src/brindle/watch_costs.py +303 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/workspaces.py +58 -0
- brindle-0.1.0/src/brindle/builtin_agents/developer-codex.md +0 -21
- brindle-0.1.0/src/brindle/builtin_agents/developer-heavy.md +0 -23
- {brindle-0.1.0 → brindle-0.2.1}/LICENSE +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/SCHEDULE-A +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/__init__.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/__main__.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/account.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/airgap.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/antigravity.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/builtin_agents/__init__.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/builtin_agents/backend-auditor.md +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/builtin_agents/developer-local.md +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/builtin_agents/qa.md +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/builtin_agents/subagent.md +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/builtin_agents/supervisor.md +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/builtin_rules/__init__.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/builtin_rules/security/backend.md +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/builtin_rules/style/minimal-diff.md +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/builtin_rules/tests/only.md +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/ci_federation.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/ci_hosts.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/codemap.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/codex_hook.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/conflicts.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/demo.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/detect.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/doctor.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/git.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/guardrails.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/inbox.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/learned_rules.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/legacy.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/native/__init__.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/native/client.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/native/loop.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/native/permissions.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/native/runner.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/native/serve.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/native/tools.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/permissions.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/plugins.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/policy.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pool.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/__init__.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/_files.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/audit_chain.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/audit_export.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/auth.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/cost_centers.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/credentials.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/features.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/keys.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/license.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/loopback.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/managed_models.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/org_profiles.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/orgkey.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/pro/settings_sync.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/procs.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/providers.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/quota.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/repo_cmds.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/repos.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/rewind.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/rule_checks.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/secrets.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/services.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/sessions.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/sidebar_follow.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/status_cache.py +0 -0
- {brindle-0.1.0 → brindle-0.2.1}/src/brindle/view.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: brindle
|
|
3
|
-
Version: 0.1
|
|
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
|
|
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
|
|
441
|
-
|
|
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` | `
|
|
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
|
|
1552
|
-
of GPU memory, so
|
|
1553
|
-
|
|
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
|
|
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
|
|
411
|
-
|
|
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` | `
|
|
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
|
|
1522
|
-
of GPU memory, so
|
|
1523
|
-
|
|
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
|
|
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 = [
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
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
|
|
1753
|
-
task += ("\n\nThe repo's checks
|
|
1754
|
-
"
|
|
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
|
|