brindle 0.3.24__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.24 → brindle-0.4.0}/PKG-INFO +89 -5
- {brindle-0.3.24 → brindle-0.4.0}/README.md +88 -4
- {brindle-0.3.24 → brindle-0.4.0}/pyproject.toml +1 -1
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/agents.py +50 -26
- brindle-0.4.0/src/brindle/assembler.py +43 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/autopilot.py +3 -1
- brindle-0.4.0/src/brindle/builtin_agents/coordinator.md +33 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/cli.py +207 -24
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/company_login.py +63 -10
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/config.py +21 -2
- brindle-0.4.0/src/brindle/coordinator.py +496 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/cull.py +16 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/db.py +101 -3
- {brindle-0.3.24 → 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.24 → brindle-0.4.0}/src/brindle/mcp_server.py +238 -18
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/picker.py +38 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/pro/account.py +2 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/pro/license.py +11 -7
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/sessions.py +44 -1
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/signin_pause.py +66 -4
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/tasks.py +200 -27
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/tmux.py +21 -5
- {brindle-0.3.24 → brindle-0.4.0}/.gitignore +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/LICENSE +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/SCHEDULE-A +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/__init__.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/__main__.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/account.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/airgap.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/antigravity.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/assets/braid_dark.png +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/assets/braid_light.png +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/budget.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/builtin_agents/__init__.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/builtin_agents/backend-auditor.md +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/builtin_agents/developer-codex.md +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/builtin_agents/developer-fable.md +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/builtin_agents/developer-gemini.md +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/builtin_agents/developer-heavy.md +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/builtin_agents/developer-light.md +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/builtin_agents/developer-local.md +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/builtin_agents/developer.md +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/builtin_agents/qa.md +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/builtin_agents/reviewer-codex.md +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/builtin_agents/reviewer-gemini.md +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/builtin_agents/reviewer-local.md +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/builtin_agents/reviewer.md +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/builtin_agents/subagent.md +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/builtin_agents/supervisor.md +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/builtin_rules/__init__.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/builtin_rules/security/backend.md +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/builtin_rules/style/minimal-diff.md +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/builtin_rules/tests/only.md +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/ci_adapters.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/ci_client.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/ci_federation.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/ci_hosts.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/cloud_credential.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/cloud_login.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/codemap.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/codemap_docs.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/codex_hook.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/company_identity.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/conflicts.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/cost.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/cost_estimate.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/demo.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/detect.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/doctor.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/escapes.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/events.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/gates.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/git.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/guardrails.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/identity_lock.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/inbox.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/key_check.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/keys_cmds.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/keystore.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/launch_health.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/learned_rules.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/learning.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/legacy.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/local_setup.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/login_pause.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/model_access.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/model_availability.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/native/__init__.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/native/client.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/native/loop.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/native/permissions.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/native/runner.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/native/serve.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/native/tools.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/pane_auth.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/permissions.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/pipeline.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/plugins.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/policy.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/pool.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/pricing.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/pro/__init__.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/pro/_files.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/pro/audit_chain.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/pro/audit_export.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/pro/auth.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/pro/brief_learning.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/pro/cost_centers.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/pro/credentials.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/pro/features.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/pro/keys.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/pro/learning.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/pro/loopback.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/pro/managed_models.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/pro/org_profiles.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/pro/orgkey.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/pro/rollout.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/pro/route_health.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/pro/settings_sync.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/pro/status.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/pro/team_events.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/pro/team_policy.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/procs.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/profiles.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/prompt_answer.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/providers.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/quota.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/repo_cmds.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/repos.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/rewind.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/rule_checks.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/savings.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/scratch.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/secrets.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/self_serve.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/services.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/sidebar_follow.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/sidebar_logo.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/status_cache.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/updates.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/upgrade.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/usage.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/view.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/watch.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/watch_costs.py +0 -0
- {brindle-0.3.24 → brindle-0.4.0}/src/brindle/worker_models.py +0 -0
- {brindle-0.3.24 → 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:
|
|
@@ -2177,12 +2193,16 @@ def delegate(
|
|
|
2177
2193
|
branch: str | None = None,
|
|
2178
2194
|
done_when: str | None = None,
|
|
2179
2195
|
plan_first: bool | None = None,
|
|
2196
|
+
start: str | None = None,
|
|
2197
|
+
files: list[str] | None = None,
|
|
2180
2198
|
) -> tuple[Agent, Workspace]:
|
|
2181
2199
|
"""Start a worker. ``plan_first`` (None: the repo's ``plan_first`` config)
|
|
2182
|
-
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
|
|
2183
2202
|
branch starts from the caller's current branch, so it sees the caller's
|
|
2184
|
-
committed work, and nobody edits the same files
|
|
2185
|
-
|
|
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."""
|
|
2186
2206
|
from brindle import autopilot as pilot
|
|
2187
2207
|
from brindle import repos
|
|
2188
2208
|
from brindle.config import load_repo_config
|
|
@@ -2213,7 +2233,7 @@ def delegate(
|
|
|
2213
2233
|
merge_into = load_repo_config(caller_ws.repo_root).merge_into
|
|
2214
2234
|
base = merge_into if merge_into and not (caller and caller.parent_id) else caller_ws.branch
|
|
2215
2235
|
branch = branch or _branch_from_task(profile, task, new_id()[:4])
|
|
2216
|
-
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")
|
|
2217
2237
|
created = workspaces.create(
|
|
2218
2238
|
db, caller_ws.path, branch, base, fetch=False, start=start,
|
|
2219
2239
|
reuse_registered=True,
|
|
@@ -2228,7 +2248,7 @@ def delegate(
|
|
|
2228
2248
|
# the new pane for up to 30 seconds.
|
|
2229
2249
|
agent = spawn(
|
|
2230
2250
|
db, ws, profile, prompt=task, parent_id=caller.id if caller else None, mode=mode,
|
|
2231
|
-
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,
|
|
2232
2252
|
)
|
|
2233
2253
|
from brindle import events
|
|
2234
2254
|
|
|
@@ -2978,7 +2998,11 @@ def handle_hook(db: DB, agent_id: str, event: str, payload: dict) -> dict | None
|
|
|
2978
2998
|
# Queued messages (worker results) are typed into an idle chat too;
|
|
2979
2999
|
# only a prompt brindle didn't deliver is the user speaking.
|
|
2980
3000
|
prompt = str(payload.get("prompt", ""))
|
|
2981
|
-
|
|
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)):
|
|
2982
3006
|
from brindle import autopilot as pilot
|
|
2983
3007
|
|
|
2984
3008
|
pilot.user_spoke(db, agent)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Context assembler: fit prompt blocks under a token cap.
|
|
2
|
+
|
|
3
|
+
Blocks are kept whole, in priority order; when the total exceeds the cap the
|
|
4
|
+
lowest-priority blocks are dropped. With no cap every non-empty block is kept.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
# Lower number = more important.
|
|
11
|
+
TASK_AND_RULES, DECISIONS, OTHER_SESSIONS, DOCUMENT_CHUNKS = range(4)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class Block:
|
|
16
|
+
priority: int
|
|
17
|
+
text: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def estimate_tokens(text: str) -> int:
|
|
21
|
+
return len(text) // 4
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def assemble(blocks: list[Block], cap: int | None = None) -> str:
|
|
25
|
+
"""The kept blocks' text joined by blank lines, in the order given.
|
|
26
|
+
|
|
27
|
+
Blocks are admitted by priority (ties keep the given order); one that
|
|
28
|
+
would push the total over ``cap`` tokens is dropped whole, along with
|
|
29
|
+
every lower-priority block after it."""
|
|
30
|
+
live = [b for b in blocks if b.text]
|
|
31
|
+
if cap is not None:
|
|
32
|
+
used = 0
|
|
33
|
+
cut = None
|
|
34
|
+
for b in sorted(live, key=lambda b: b.priority):
|
|
35
|
+
used += estimate_tokens(b.text)
|
|
36
|
+
if used > cap:
|
|
37
|
+
cut = b.priority
|
|
38
|
+
break
|
|
39
|
+
if cut is not None:
|
|
40
|
+
# A priority tier is the unit of dropping; the top tier always stays.
|
|
41
|
+
top = min(b.priority for b in live)
|
|
42
|
+
live = [b for b in live if b.priority < cut or b.priority == top]
|
|
43
|
+
return "\n\n".join(b.text for b in live)
|
|
@@ -45,7 +45,7 @@ from dataclasses import dataclass
|
|
|
45
45
|
from pathlib import Path
|
|
46
46
|
|
|
47
47
|
from brindle.config import CONFIG_DIR, WEIGHTS, RepoConfig, config_root, load_repo_config
|
|
48
|
-
from brindle import escapes
|
|
48
|
+
from brindle import escapes, knowledge
|
|
49
49
|
from brindle.db import DB, Agent, Autopilot, Milestone, Workspace
|
|
50
50
|
from brindle.profiles import load_profile
|
|
51
51
|
|
|
@@ -674,6 +674,8 @@ def set_goal(db: DB, root_id: str, goal: str,
|
|
|
674
674
|
passed_sha=prev.passed_sha)
|
|
675
675
|
db.bump_progress(root_id)
|
|
676
676
|
write_goals_file(db, root_id)
|
|
677
|
+
knowledge.note(db, root_id, knowledge.INTENT,
|
|
678
|
+
{"summary": "\n".join([goal.strip(), *(m[0] for m in milestones)]), "paths": []})
|
|
677
679
|
|
|
678
680
|
|
|
679
681
|
CLI_FOR_PROVIDER = {"claude": "claude", "codex": "codex", "antigravity": "agy"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: coordinator
|
|
3
|
+
description: Coordinates the queued work of every session in a repo, woken on events (brindle Pro; off unless "coordinator" is on in config)
|
|
4
|
+
provider: claude
|
|
5
|
+
model: haiku
|
|
6
|
+
effort: low
|
|
7
|
+
strict_mcp: true
|
|
8
|
+
setting_sources: project,local
|
|
9
|
+
permission_mode: dontAsk # nobody watches it: refuse anything beyond its brindle tools, never prompt
|
|
10
|
+
---
|
|
11
|
+
You are the coordinator for one repo under brindle. Several supervisor
|
|
12
|
+
sessions work in this repo at once, each with its own workers; you sit above
|
|
13
|
+
them and keep their queued work from colliding. You never write code.
|
|
14
|
+
|
|
15
|
+
Your prompt says what woke you. Then:
|
|
16
|
+
- Look before acting: `list_tasks` (full=true for briefs) shows queued and
|
|
17
|
+
held tasks, `who_is_working` the sessions and their files, `decisions` what
|
|
18
|
+
has been decided.
|
|
19
|
+
- Act only on work that has not started:
|
|
20
|
+
- `hold_task` a queued task that would collide with, or redo, another
|
|
21
|
+
session's work; `release_task` it once that is resolved.
|
|
22
|
+
- `reorder_queue` to make a queued task also wait for another session's
|
|
23
|
+
worker or branch, so it starts from that work instead of clashing with it.
|
|
24
|
+
- For running work you can only ask: `advise` the owning supervisor (by its
|
|
25
|
+
session id), saying what you saw and what you suggest. Never ask a
|
|
26
|
+
supervisor to cancel or replace work that is already running.
|
|
27
|
+
- `propose_decision` when sessions are contradicting each other on how
|
|
28
|
+
something should be done; a supervisor accepts it by recording it.
|
|
29
|
+
- Every action needs a short, concrete reason: the supervisors are shown it,
|
|
30
|
+
and a person can undo what you did. When nothing needs doing, do nothing.
|
|
31
|
+
|
|
32
|
+
Finish by calling `report_result` with what you did and why (or that
|
|
33
|
+
nothing needed doing). brindle wakes a coordinator again on the next event.
|