brindle 0.3.20__tar.gz → 0.3.22__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.20 → brindle-0.3.22}/PKG-INFO +2 -2
- {brindle-0.3.20 → brindle-0.3.22}/README.md +1 -1
- {brindle-0.3.20 → brindle-0.3.22}/pyproject.toml +1 -1
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/agents.py +112 -21
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/cli.py +15 -1
- brindle-0.3.22/src/brindle/cloud_login.py +497 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/db.py +17 -1
- brindle-0.3.22/src/brindle/login_pause.py +330 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/model_access.py +2 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/providers.py +5 -1
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/tasks.py +56 -23
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/tmux.py +10 -0
- brindle-0.3.20/src/brindle/cloud_login.py +0 -225
- brindle-0.3.20/src/brindle/login_pause.py +0 -223
- {brindle-0.3.20 → brindle-0.3.22}/.gitignore +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/LICENSE +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/SCHEDULE-A +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/__init__.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/__main__.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/account.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/airgap.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/antigravity.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/autopilot.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/budget.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/builtin_agents/__init__.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/builtin_agents/backend-auditor.md +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/builtin_agents/developer-codex.md +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/builtin_agents/developer-fable.md +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/builtin_agents/developer-gemini.md +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/builtin_agents/developer-heavy.md +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/builtin_agents/developer-light.md +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/builtin_agents/developer-local.md +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/builtin_agents/developer.md +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/builtin_agents/qa.md +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/builtin_agents/reviewer-codex.md +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/builtin_agents/reviewer-gemini.md +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/builtin_agents/reviewer-local.md +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/builtin_agents/reviewer.md +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/builtin_agents/subagent.md +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/builtin_agents/supervisor.md +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/builtin_rules/__init__.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/builtin_rules/security/backend.md +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/builtin_rules/style/minimal-diff.md +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/builtin_rules/tests/only.md +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/ci_adapters.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/ci_client.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/ci_federation.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/ci_hosts.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/cloud_credential.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/codemap.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/codemap_docs.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/codex_hook.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/company_identity.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/company_login.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/config.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/conflicts.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/cost.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/cost_estimate.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/cull.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/demo.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/detect.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/doctor.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/escapes.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/events.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/gates.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/git.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/guardrails.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/history.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/identity_lock.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/inbox.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/key_check.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/keys_cmds.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/keystore.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/launch_health.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/learned_rules.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/learning.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/legacy.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/local_setup.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/mcp_server.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/model_availability.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/native/__init__.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/native/client.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/native/loop.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/native/permissions.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/native/runner.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/native/serve.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/native/tools.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/pane_auth.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/permissions.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/picker.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/pipeline.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/plugins.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/policy.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/pool.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/pricing.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/pro/__init__.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/pro/_files.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/pro/account.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/pro/audit_chain.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/pro/audit_export.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/pro/auth.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/pro/brief_learning.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/pro/cost_centers.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/pro/credentials.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/pro/features.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/pro/keys.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/pro/learning.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/pro/license.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/pro/loopback.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/pro/managed_models.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/pro/org_profiles.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/pro/orgkey.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/pro/rollout.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/pro/route_health.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/pro/settings_sync.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/pro/status.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/pro/team_events.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/pro/team_policy.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/procs.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/profiles.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/prompt_answer.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/quota.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/repo_cmds.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/repos.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/rewind.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/rule_checks.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/savings.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/scratch.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/secrets.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/self_serve.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/services.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/sessions.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/sidebar_follow.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/signin_pause.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/status_cache.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/updates.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/upgrade.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/usage.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/view.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/watch.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/watch_costs.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/worker_models.py +0 -0
- {brindle-0.3.20 → brindle-0.3.22}/src/brindle/workspaces.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: brindle
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.22
|
|
4
4
|
Summary: Run coding agents in parallel, each on its own git branch, with a supervisor that delegates, reviews, and merges
|
|
5
5
|
Project-URL: Homepage, https://pawdelta.com/brindle/
|
|
6
6
|
Project-URL: Documentation, https://pawdelta.com/brindle/docs/recommended-use
|
|
@@ -94,7 +94,7 @@ uv tool install --editable ~/Projects/brindle # or from a local checkout
|
|
|
94
94
|
brindle drives Claude Code, so you need that too (`npm install -g @anthropic-ai/claude-code`).
|
|
95
95
|
|
|
96
96
|
`brindle --version` prints the installed version; the tmux status bar of every brindle
|
|
97
|
-
session shows it too (`brindle 0.3.
|
|
97
|
+
session shows it too (`brindle 0.3.22`). 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
|
|
@@ -63,7 +63,7 @@ uv tool install --editable ~/Projects/brindle # or from a local checkout
|
|
|
63
63
|
brindle drives Claude Code, so you need that too (`npm install -g @anthropic-ai/claude-code`).
|
|
64
64
|
|
|
65
65
|
`brindle --version` prints the installed version; the tmux status bar of every brindle
|
|
66
|
-
session shows it too (`brindle 0.3.
|
|
66
|
+
session shows it too (`brindle 0.3.22`). 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
|
|
@@ -19,6 +19,7 @@ import subprocess
|
|
|
19
19
|
import sys
|
|
20
20
|
import time
|
|
21
21
|
import uuid
|
|
22
|
+
from types import SimpleNamespace
|
|
22
23
|
|
|
23
24
|
from brindle import cloud_credential, company_identity, company_login, git, keystore, pane_auth, providers, secrets, tmux, workspaces
|
|
24
25
|
from brindle.config import RepoConfig
|
|
@@ -981,11 +982,12 @@ def run_headless(db: DB, agent_id: str, resume: str | None = None, *,
|
|
|
981
982
|
provider = get_provider(agent.provider)
|
|
982
983
|
session = resume
|
|
983
984
|
turn = 0
|
|
985
|
+
retry = None # a turn the cloud login failed: run again before anything else
|
|
984
986
|
while True:
|
|
985
987
|
agent = db.get_agent(agent_id)
|
|
986
988
|
if agent is None:
|
|
987
989
|
return 0
|
|
988
|
-
msg = db.pop_pending(agent_id)
|
|
990
|
+
msg, retry = retry or db.pop_pending(agent_id), None
|
|
989
991
|
if msg is None:
|
|
990
992
|
if exit_when_idle:
|
|
991
993
|
return 0
|
|
@@ -1014,12 +1016,46 @@ def run_headless(db: DB, agent_id: str, resume: str | None = None, *,
|
|
|
1014
1016
|
return 0
|
|
1015
1017
|
session = agent.session_ref or session
|
|
1016
1018
|
if code != 0:
|
|
1019
|
+
if wait_for_cloud_login(db, agent, ws):
|
|
1020
|
+
# The same turn again, from where it stopped.
|
|
1021
|
+
retry = SimpleNamespace(body=HEADLESS_CONTINUE) if session else msg
|
|
1022
|
+
continue
|
|
1017
1023
|
print(f"\n── brindle: claude exited with code {code}; this worker has stopped ──", flush=True)
|
|
1018
1024
|
return code
|
|
1019
1025
|
db.set_status(agent_id, "idle", only_if="processing")
|
|
1020
1026
|
print("── brindle: turn finished; waiting for messages ──", flush=True)
|
|
1021
1027
|
|
|
1022
1028
|
|
|
1029
|
+
def wait_for_cloud_login(db: DB, agent: Agent, ws: Workspace, poll: float = 60.0) -> bool:
|
|
1030
|
+
"""A headless turn failed. If it was on a lapsed cloud login, wait here
|
|
1031
|
+
(offering the one browser sign-in, see cloud_login.sign_in) until the
|
|
1032
|
+
login works again and return True; False for any other failure."""
|
|
1033
|
+
from brindle import cloud_login
|
|
1034
|
+
|
|
1035
|
+
try:
|
|
1036
|
+
env = agent_env(ws, agent.id, agent)
|
|
1037
|
+
except Exception: # noqa: BLE001 - an unreadable env: not ours to wait on
|
|
1038
|
+
return False
|
|
1039
|
+
if cloud_login.provider_from_env({**os.environ, **env}) is None:
|
|
1040
|
+
return False
|
|
1041
|
+
s = cloud_login.refresh(env=env)
|
|
1042
|
+
if not s.expired:
|
|
1043
|
+
return False
|
|
1044
|
+
print("\n── brindle: the cloud login lapsed; this worker waits until it's renewed "
|
|
1045
|
+
f"(sign in with the browser tab brindle opens, or run `{s.refresh_cmd}`) ──", flush=True)
|
|
1046
|
+
while db.get_agent(agent.id) is not None:
|
|
1047
|
+
try:
|
|
1048
|
+
last_input = tmux.last_input()
|
|
1049
|
+
except tmux.TmuxError:
|
|
1050
|
+
last_input = None
|
|
1051
|
+
cloud_login.sign_in(env=env, last_input=last_input)
|
|
1052
|
+
if cloud_login.status(env=env).ok:
|
|
1053
|
+
print("── brindle: cloud login renewed; continuing ──", flush=True)
|
|
1054
|
+
return True
|
|
1055
|
+
time.sleep(poll)
|
|
1056
|
+
return False
|
|
1057
|
+
|
|
1058
|
+
|
|
1023
1059
|
def ready(db: DB, agent_id: str) -> None:
|
|
1024
1060
|
"""after_launch saw the CLI's input box. For CLIs with no hook that says
|
|
1025
1061
|
so, this is when it's ready (and when queued messages can go in)."""
|
|
@@ -1867,6 +1903,16 @@ def _detach(args: list[str]) -> None:
|
|
|
1867
1903
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
1868
1904
|
|
|
1869
1905
|
|
|
1906
|
+
def _detach_orphan(args: list[str]) -> None:
|
|
1907
|
+
"""Start a brindle helper that outlives this process and its agent: a
|
|
1908
|
+
shell starts it in the background and exits, so it is nobody's child.
|
|
1909
|
+
Stopping an agent stops every process descended from its MCP server
|
|
1910
|
+
(brindle.procs), a helper ``_detach`` started there included."""
|
|
1911
|
+
subprocess.run(["/bin/sh", "-c", '"$@" </dev/null >/dev/null 2>&1 &', "sh", *args],
|
|
1912
|
+
start_new_session=True, stdin=subprocess.DEVNULL,
|
|
1913
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False)
|
|
1914
|
+
|
|
1915
|
+
|
|
1870
1916
|
def wait_for_result(db: DB, agent_id: str, timeout: float, poll: float = 2.0) -> str:
|
|
1871
1917
|
deadline = time.time() + timeout
|
|
1872
1918
|
while time.time() < deadline:
|
|
@@ -1935,37 +1981,73 @@ def submit_review(db: DB, caller_id: str, approved: bool, summary: str) -> str:
|
|
|
1935
1981
|
db.enqueue(root_id, text, None) # handed over at the supervisor's next Stop
|
|
1936
1982
|
close_later(caller.id)
|
|
1937
1983
|
return "Audit recorded and sent to the supervisor. You're done."
|
|
1984
|
+
# Already recorded: closing this reviewer is its verdict's job, once
|
|
1985
|
+
# whatever the verdict set off is done. Checked before the lock too, which
|
|
1986
|
+
# that work (a merge running the checks) can hold for minutes.
|
|
1987
|
+
recorded = "Your review was already recorded. You're done."
|
|
1988
|
+
if caller.result is not None:
|
|
1989
|
+
return recorded
|
|
1938
1990
|
# One verdict at a time per workspace: a second one (the same reviewer
|
|
1939
1991
|
# calling twice, or another reviewer of the same branch) waits here, then
|
|
1940
1992
|
# finds what the first one did instead of merging alongside it.
|
|
1941
1993
|
with pipeline.merge_lock(ws.id):
|
|
1942
1994
|
caller = db.get_agent(caller_id)
|
|
1943
|
-
if caller is None
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
close_later(caller_id)
|
|
1947
|
-
return "
|
|
1995
|
+
if caller is not None and caller.result is not None:
|
|
1996
|
+
return recorded
|
|
1997
|
+
if caller is None or db.get_workspace(ws.id) is None or not os.path.isdir(ws.path):
|
|
1998
|
+
close_later(caller_id) # the branch merged (or was removed) meanwhile
|
|
1999
|
+
return "The branch is already merged or removed. You're done."
|
|
1948
2000
|
sha = gates.head(ws)
|
|
1949
2001
|
db.add_review(ws.id, sha, caller.id, approved, summary)
|
|
1950
2002
|
if approved:
|
|
1951
2003
|
db.bump_progress(autopilot.root_of(db, caller.id))
|
|
1952
2004
|
pipeline.note_review(db, ws, approved, reviewer=caller)
|
|
1953
2005
|
verdict = "APPROVED" if approved else "CHANGES REQUESTED"
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
2006
|
+
db.set_result(caller.id, _review_text(ws, sha, approved, summary))
|
|
2007
|
+
finish_review_later(caller.id, ws.id)
|
|
2008
|
+
return f"Review recorded ({verdict}); brindle takes it from here. You're done."
|
|
2009
|
+
|
|
2010
|
+
|
|
2011
|
+
def _review_text(ws: Workspace, sha: str, approved: bool, summary: str) -> str:
|
|
2012
|
+
verdict = "APPROVED" if approved else "CHANGES REQUESTED"
|
|
2013
|
+
return f"Review of {ws.branch} (workspace {ws.id}) at {sha[:8]}: {verdict}\n\n{summary}"
|
|
2014
|
+
|
|
2015
|
+
|
|
2016
|
+
def finish_review_later(reviewer_id: str, ws_id: str) -> None:
|
|
2017
|
+
"""Act on a recorded verdict (``finish_review``) from a process of its
|
|
2018
|
+
own. Not in the reviewer's MCP server: a merge runs the repo's checks,
|
|
2019
|
+
which can take minutes, and a reviewer kept waiting that long calls
|
|
2020
|
+
submit_review again, or is closed, which would stop the merge, the
|
|
2021
|
+
queued tasks it starts and the supervisor's notice half way."""
|
|
2022
|
+
from brindle.providers import brindle_invocation
|
|
2023
|
+
|
|
2024
|
+
_detach_orphan([*brindle_invocation(), "_finish-review", reviewer_id, ws_id])
|
|
2025
|
+
|
|
2026
|
+
|
|
2027
|
+
def finish_review(db: DB, reviewer_id: str, ws_id: str) -> None:
|
|
2028
|
+
"""Hand a verdict submit_review recorded to the pipeline if the branch
|
|
2029
|
+
is piped, else to the supervisor, then close the reviewer."""
|
|
2030
|
+
from brindle import pipeline
|
|
2031
|
+
|
|
2032
|
+
try:
|
|
2033
|
+
caller, ws = db.get_agent(reviewer_id), db.get_workspace(ws_id)
|
|
2034
|
+
review = next((r for r in reversed(db.workspace_reviews(ws_id))
|
|
2035
|
+
if r.reviewer_id == reviewer_id), None) if caller and ws else None
|
|
2036
|
+
if review is None:
|
|
2037
|
+
return
|
|
2038
|
+
# Outside the lock: an approval merges every ready branch of the base,
|
|
2039
|
+
# taking each one's lock in turn, and holding this branch's lock while
|
|
2040
|
+
# waiting for that would deadlock with a drain that wants it.
|
|
2041
|
+
handled = pipeline.on_review(db, caller, ws, review.approved, review.summary or "")
|
|
2042
|
+
# A merge the pipeline just made removes this reviewer's own record.
|
|
2043
|
+
text = _review_text(ws, review.sha, review.approved, review.summary or "")
|
|
2044
|
+
report_result(db, caller.id, text, forward=not handled, removed=(caller, ws))
|
|
2045
|
+
if not review.approved:
|
|
2046
|
+
from brindle import learned_rules
|
|
2047
|
+
|
|
2048
|
+
learned_rules.refresh_later(ws.repo_root) # the finding just landed in the history
|
|
2049
|
+
finally:
|
|
2050
|
+
close_later(reviewer_id)
|
|
1969
2051
|
|
|
1970
2052
|
|
|
1971
2053
|
def close_later(agent_id: str, delay: float = 5.0) -> None:
|
|
@@ -2713,6 +2795,15 @@ def _stop_decision(db: DB, agent: Agent, payload: dict, via_inbox: bool = False)
|
|
|
2713
2795
|
inbox message rather than block the stop, so ``stop_hook_active`` never
|
|
2714
2796
|
caps a repeat; the unread-mail reminder is then limited to one per
|
|
2715
2797
|
NOTICE_TTL (DB.claim_stop_reminder)."""
|
|
2798
|
+
if agent.mode == "interactive":
|
|
2799
|
+
# Every supervisor turn ends here: start any queued task a cut-off or
|
|
2800
|
+
# manual merge left waiting (its "Started" note is delivered below).
|
|
2801
|
+
try:
|
|
2802
|
+
from brindle import tasks
|
|
2803
|
+
|
|
2804
|
+
tasks.sweep(db)
|
|
2805
|
+
except Exception: # noqa: BLE001 -- must never break the Stop hook
|
|
2806
|
+
log.exception("brindle: queued-task sweep failed")
|
|
2716
2807
|
msg = db.pop_pending(agent.id)
|
|
2717
2808
|
if msg:
|
|
2718
2809
|
db.set_status(agent.id, "processing")
|
|
@@ -589,7 +589,12 @@ def _pause_running(db: DB, ws: Workspace, stop_procs: bool = True) -> None:
|
|
|
589
589
|
``stop_procs=False`` when a detached cull follows (see agents.pause)."""
|
|
590
590
|
from brindle import scratch
|
|
591
591
|
|
|
592
|
-
|
|
592
|
+
live = _live_sessions(db, ws)
|
|
593
|
+
caller = os.environ.get("BRINDLE_AGENT_ID")
|
|
594
|
+
if caller and any(a.id == caller for a in live):
|
|
595
|
+
# Pausing kills the session's process tree, this command with it.
|
|
596
|
+
_fail("this command would stop the session running it; run it from a terminal instead")
|
|
597
|
+
for a in live:
|
|
593
598
|
# A session starts here next: keep its local models loaded.
|
|
594
599
|
agents.pause(db, a.id, stop_procs=stop_procs, stop_local_models=False)
|
|
595
600
|
typer.echo(f"Paused the session that was still running here ({a.id}); "
|
|
@@ -2814,6 +2819,15 @@ def close_cmd(agent_id: str, delay: float = typer.Option(0.0)) -> None:
|
|
|
2814
2819
|
pass
|
|
2815
2820
|
|
|
2816
2821
|
|
|
2822
|
+
@app.command("_finish-review", hidden=True)
|
|
2823
|
+
def finish_review_cmd(reviewer_id: str, workspace_id: str) -> None:
|
|
2824
|
+
import signal
|
|
2825
|
+
|
|
2826
|
+
# Started by the reviewer's submit_review, and must outlive its close.
|
|
2827
|
+
signal.signal(signal.SIGHUP, signal.SIG_IGN)
|
|
2828
|
+
agents.finish_review(_helper_db(), reviewer_id, workspace_id)
|
|
2829
|
+
|
|
2830
|
+
|
|
2817
2831
|
@app.command("_statusline", hidden=True)
|
|
2818
2832
|
def statusline_cmd() -> None:
|
|
2819
2833
|
from brindle.providers import status_line
|