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