brindle 0.4.3__tar.gz → 0.4.5__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.4.3 → brindle-0.4.5}/PKG-INFO +1 -1
- {brindle-0.4.3 → brindle-0.4.5}/pyproject.toml +1 -1
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/agents.py +42 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/cli.py +1 -4
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/codex_hook.py +11 -1
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/sessions.py +16 -1
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/sidebar_follow.py +55 -4
- {brindle-0.4.3 → brindle-0.4.5}/.gitignore +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/LICENSE +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/README.md +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/SCHEDULE-A +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/__init__.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/__main__.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/account.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/airgap.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/antigravity.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/assembler.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/assets/braid_dark.png +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/assets/braid_light.png +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/autopilot.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/budget.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/builtin_agents/__init__.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/builtin_agents/backend-auditor.md +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/builtin_agents/coordinator.md +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/builtin_agents/developer-codex.md +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/builtin_agents/developer-fable.md +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/builtin_agents/developer-gemini.md +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/builtin_agents/developer-heavy.md +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/builtin_agents/developer-light.md +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/builtin_agents/developer-local.md +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/builtin_agents/developer-opencode.md +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/builtin_agents/developer-sonnet.md +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/builtin_agents/developer.md +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/builtin_agents/qa.md +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/builtin_agents/reviewer-codex.md +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/builtin_agents/reviewer-gemini.md +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/builtin_agents/reviewer-local.md +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/builtin_agents/reviewer-opencode.md +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/builtin_agents/reviewer.md +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/builtin_agents/subagent.md +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/builtin_agents/supervisor.md +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/builtin_rules/__init__.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/builtin_rules/security/backend.md +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/builtin_rules/style/minimal-diff.md +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/builtin_rules/tests/only.md +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/ci_adapters.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/ci_client.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/ci_federation.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/ci_hosts.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/cloud_credential.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/cloud_login.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/codemap.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/codemap_docs.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/company_identity.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/company_login.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/config.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/conflicts.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/coordinator.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/cost.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/cost_estimate.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/cull.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/db.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/demo.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/detect.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/doctor.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/escapes.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/events.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/gates.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/git.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/guardrails.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/history.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/identity_lock.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/inbox.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/kb.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/key_check.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/keys_cmds.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/keystore.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/knowledge.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/launch_health.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/learned_rules.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/learning.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/legacy.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/local_setup.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/login_pause.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/mcp_server.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/model_access.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/model_availability.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/native/__init__.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/native/catalog.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/native/client.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/native/loop.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/native/lsp.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/native/permissions.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/native/runner.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/native/serve.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/native/tools.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/opencode.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/pane_auth.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/permissions.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/picker.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/pipeline.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/plugins.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/policy.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/pool.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/pricing.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/pro/__init__.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/pro/_files.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/pro/account.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/pro/audit_chain.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/pro/audit_export.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/pro/auth.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/pro/brief_learning.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/pro/cost_centers.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/pro/credentials.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/pro/features.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/pro/keys.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/pro/learning.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/pro/license.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/pro/loopback.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/pro/managed_models.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/pro/org_profiles.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/pro/orgkey.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/pro/rollout.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/pro/route_health.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/pro/settings_sync.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/pro/status.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/pro/team_events.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/pro/team_policy.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/procs.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/profiles.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/prompt_answer.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/providers.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/quota.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/repo_cmds.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/repos.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/rewind.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/rule_checks.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/savings.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/scratch.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/secrets.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/self_serve.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/services.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/sidebar_logo.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/signin_pause.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/status_cache.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/tasks.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/tmux.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/updates.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/upgrade.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/usage.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/view.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/wake.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/watch.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/watch_costs.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/web/__init__.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/web/chat.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/web/dashboard.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/web/diff.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/web/server.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/web/static/app.js +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/web/static/index.html +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/worker_models.py +0 -0
- {brindle-0.4.3 → brindle-0.4.5}/src/brindle/workspaces.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: brindle
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.5
|
|
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
|
|
@@ -2159,9 +2159,45 @@ def finish_review(db: DB, reviewer_id: str, ws_id: str) -> None:
|
|
|
2159
2159
|
|
|
2160
2160
|
learned_rules.refresh_later(ws.repo_root) # the finding just landed in the history
|
|
2161
2161
|
finally:
|
|
2162
|
+
# A merge removes the workspace but leaves the session this reviewer
|
|
2163
|
+
# runs in; once the reviewer is gone nobody else would end it.
|
|
2164
|
+
if ws and db.get_workspace(ws_id) is None:
|
|
2165
|
+
_closing_session(reviewer_id).write_text(ws.tmux_session)
|
|
2162
2166
|
close_later(reviewer_id)
|
|
2163
2167
|
|
|
2164
2168
|
|
|
2169
|
+
def _closing_session(agent_id: str):
|
|
2170
|
+
from brindle.config import brindle_home
|
|
2171
|
+
|
|
2172
|
+
d = brindle_home() / "closing"
|
|
2173
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
2174
|
+
return d / agent_id
|
|
2175
|
+
|
|
2176
|
+
|
|
2177
|
+
def close_removed(db: DB, agent_id: str) -> None:
|
|
2178
|
+
"""Kill an agent and, when its workspace was removed with the session left
|
|
2179
|
+
running (noted by ``finish_review``), that session too: only brindle's own,
|
|
2180
|
+
unattached session of a workspace that no longer exists."""
|
|
2181
|
+
note = _closing_session(agent_id)
|
|
2182
|
+
try:
|
|
2183
|
+
session = note.read_text().strip()
|
|
2184
|
+
note.unlink()
|
|
2185
|
+
except OSError:
|
|
2186
|
+
session = ""
|
|
2187
|
+
root_id = root_of(db, agent_id) # before kill drops the record that links it
|
|
2188
|
+
try:
|
|
2189
|
+
kill(db, agent_id)
|
|
2190
|
+
except AgentError:
|
|
2191
|
+
pass
|
|
2192
|
+
if session and db.workspace_by_tmux_session(session) is None \
|
|
2193
|
+
and not tmux.session_attached(session):
|
|
2194
|
+
# A sidebar left in this session would go down with it.
|
|
2195
|
+
sidebar = db.get_sidebar_pane(root_id)
|
|
2196
|
+
if sidebar and sidebar != SIDEBAR_DISMISSED and tmux.pane_session(sidebar) == session:
|
|
2197
|
+
sidebar_come_home(db, root_id, sidebar)
|
|
2198
|
+
tmux.kill_session(session)
|
|
2199
|
+
|
|
2200
|
+
|
|
2165
2201
|
def close_later(agent_id: str, delay: float = 5.0) -> None:
|
|
2166
2202
|
"""Stop an agent shortly, from a detached process: used by an agent's own
|
|
2167
2203
|
tool call, which must return before its CLI goes away."""
|
|
@@ -2182,6 +2218,12 @@ def _stop(db: DB, agent: Agent) -> None:
|
|
|
2182
2218
|
# Only a window on the server brindle is using: another server's pane
|
|
2183
2219
|
# ids mean other panes here (see same_server).
|
|
2184
2220
|
window = agent.tmux_window if same_server(agent) and owns_pane(db, agent) else ""
|
|
2221
|
+
if window:
|
|
2222
|
+
from brindle.sidebar_follow import rescue_sidebar_from_window
|
|
2223
|
+
|
|
2224
|
+
# Before the pids are read: a moved pane keeps its process, and the
|
|
2225
|
+
# sidebar's would otherwise be stopped with the agent's.
|
|
2226
|
+
rescue_sidebar_from_window(db, agent)
|
|
2185
2227
|
pane_pids = tmux.window_pids(window) if window else []
|
|
2186
2228
|
if window:
|
|
2187
2229
|
tmux.kill_window(window)
|
|
@@ -3080,10 +3080,7 @@ def quit_cmd(root_id: str) -> None:
|
|
|
3080
3080
|
@app.command("_close", hidden=True)
|
|
3081
3081
|
def close_cmd(agent_id: str, delay: float = typer.Option(0.0)) -> None:
|
|
3082
3082
|
time.sleep(delay)
|
|
3083
|
-
|
|
3084
|
-
agents.kill(_helper_db(), agent_id)
|
|
3085
|
-
except agents.AgentError:
|
|
3086
|
-
pass
|
|
3083
|
+
agents.close_removed(_helper_db(), agent_id)
|
|
3087
3084
|
|
|
3088
3085
|
|
|
3089
3086
|
@app.command("_finish-review", hidden=True)
|
|
@@ -36,6 +36,7 @@ import json
|
|
|
36
36
|
import os
|
|
37
37
|
import queue
|
|
38
38
|
import shlex
|
|
39
|
+
import signal
|
|
39
40
|
import subprocess
|
|
40
41
|
import threading
|
|
41
42
|
import time
|
|
@@ -228,7 +229,8 @@ class _AppServer:
|
|
|
228
229
|
self.timeout = timeout
|
|
229
230
|
try:
|
|
230
231
|
self.proc = subprocess.Popen([binary, "app-server", *flags], stdin=subprocess.PIPE,
|
|
231
|
-
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True
|
|
232
|
+
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True,
|
|
233
|
+
start_new_session=True)
|
|
232
234
|
except OSError as e:
|
|
233
235
|
raise CodexHookError(f"couldn't run {binary}: {e}") from e
|
|
234
236
|
self.lines: queue.Queue = queue.Queue()
|
|
@@ -279,6 +281,14 @@ class _AppServer:
|
|
|
279
281
|
return result
|
|
280
282
|
|
|
281
283
|
def close(self) -> None:
|
|
284
|
+
# The app-server leads its own process group (see __init__): kill the
|
|
285
|
+
# group so nothing it spawned outlives it.
|
|
286
|
+
pid = getattr(self.proc, "pid", None)
|
|
287
|
+
if pid:
|
|
288
|
+
try:
|
|
289
|
+
os.killpg(pid, signal.SIGKILL)
|
|
290
|
+
except OSError:
|
|
291
|
+
pass
|
|
282
292
|
try:
|
|
283
293
|
self.proc.kill()
|
|
284
294
|
except OSError:
|
|
@@ -40,10 +40,22 @@ class Session:
|
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
def paused(db: DB, repo_root: str) -> list[Session]:
|
|
43
|
-
"""Paused sessions whose chat ran in ``repo_root``, newest first.
|
|
43
|
+
"""Paused sessions whose chat ran in ``repo_root``, newest first. A
|
|
44
|
+
"failed" chat whose pane is still running isn't one: that status also
|
|
45
|
+
marks a live chat (a brief whose delivery wasn't confirmed, an auth error
|
|
46
|
+
on one turn), and dropping it (see enforce) would close it mid-session."""
|
|
44
47
|
out = []
|
|
48
|
+
panes = owners = None
|
|
45
49
|
for ws in db.find_workspaces(repo_root):
|
|
46
50
|
for a in db.list_agents(ws.id):
|
|
51
|
+
if a.mode == "interactive" and a.status == "failed":
|
|
52
|
+
if panes is None:
|
|
53
|
+
panes = tmux.list_panes()
|
|
54
|
+
owners = agents.pane_owners(db, panes)
|
|
55
|
+
# On another server, its own server's panes say whose its pane is.
|
|
56
|
+
mine = owners if agents.same_server(a, panes) else None
|
|
57
|
+
if agents.is_alive(a, panes) and agents.owns_pane(db, a, mine):
|
|
58
|
+
continue
|
|
47
59
|
if a.mode == "interactive" and a.status in ("paused", "failed"):
|
|
48
60
|
members = agents.tree(db, a.id)
|
|
49
61
|
branches = []
|
|
@@ -212,6 +224,9 @@ def _forget(db: DB, s: Session) -> None:
|
|
|
212
224
|
# some other pane here (see agents.same_server).
|
|
213
225
|
if (a.tmux_window and agents.same_server(a, panes) and agents.is_alive(a, panes)
|
|
214
226
|
and agents.owns_pane(db, a, owners)):
|
|
227
|
+
from brindle.sidebar_follow import rescue_sidebar_from_window
|
|
228
|
+
|
|
229
|
+
rescue_sidebar_from_window(db, a)
|
|
215
230
|
tmux.kill_window(a.tmux_window)
|
|
216
231
|
ws = db.get_workspace(a.workspace_id)
|
|
217
232
|
if ws and ws.kind == "worktree":
|
|
@@ -196,6 +196,54 @@ def rescue_sidebar(db: DB, ws: Workspace) -> None:
|
|
|
196
196
|
pass
|
|
197
197
|
|
|
198
198
|
|
|
199
|
+
def _home_session(db: DB, root_id: str, sidebar: str) -> str | None:
|
|
200
|
+
"""The brindle session whose agent window ``sidebar`` sits in. Not
|
|
201
|
+
``tmux.pane_session``: a window the app linked into its own
|
|
202
|
+
``brindle-view-*`` session reports that one, and that session being
|
|
203
|
+
attached says nothing about the person's terminal on the brindle session."""
|
|
204
|
+
window = tmux.pane_window(sidebar)
|
|
205
|
+
for agent in db.list_agents():
|
|
206
|
+
if (window and agent.tmux_window and root_of(db, agent.id) == root_id
|
|
207
|
+
and tmux.pane_window(agent.tmux_window) == window):
|
|
208
|
+
ws = db.get_workspace(agent.workspace_id)
|
|
209
|
+
if ws is not None:
|
|
210
|
+
return ws.tmux_session
|
|
211
|
+
return tmux.pane_session(sidebar)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def rescue_sidebar_from_window(db: DB, agent: Agent) -> None:
|
|
215
|
+
"""Call before ``agent``'s tmux window is killed: if its root's sidebar is
|
|
216
|
+
a pane in that window, move it to a window of the root's session that
|
|
217
|
+
survives (the root's active one, else any other agent's there: a worker
|
|
218
|
+
sharing the root's workspace has its window in the same session). Never
|
|
219
|
+
raises."""
|
|
220
|
+
try:
|
|
221
|
+
if not agent.tmux_window:
|
|
222
|
+
return
|
|
223
|
+
root_id = root_of(db, agent.id)
|
|
224
|
+
root = db.get_agent(root_id)
|
|
225
|
+
root_ws = db.get_workspace(root.workspace_id) if root else None
|
|
226
|
+
if root is None or root_ws is None or agent.id == root_id:
|
|
227
|
+
return
|
|
228
|
+
with _sidebar_lock(root_id):
|
|
229
|
+
sidebar = db.get_sidebar_pane(root_id)
|
|
230
|
+
doomed = tmux.pane_window(agent.tmux_window)
|
|
231
|
+
if not (_valid_sidebar(sidebar, root_id) and doomed
|
|
232
|
+
and tmux.pane_window(sidebar) == doomed):
|
|
233
|
+
return
|
|
234
|
+
candidates = [tmux.active_window(root_ws.tmux_session)]
|
|
235
|
+
candidates += [tmux.pane_window(a.tmux_window) for a in db.list_agents(root_ws.id)
|
|
236
|
+
if a.tmux_window and a.status not in ("paused", "done")]
|
|
237
|
+
for window in candidates:
|
|
238
|
+
target_pane = window and window != doomed and tmux.agent_pane_in_window(window, sidebar)
|
|
239
|
+
if target_pane:
|
|
240
|
+
_note_sidebar_move(f"rescue to {root_ws.tmux_session}", sidebar, target_pane)
|
|
241
|
+
tmux.move_pane(sidebar, target_pane, SIDEBAR_COLUMNS, _sidebar_position(root_ws))
|
|
242
|
+
return
|
|
243
|
+
except Exception: # noqa: BLE001 - cleanup must not fail because of the sidebar
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
|
|
199
247
|
def sidebar_follow(db: DB, session: str) -> None:
|
|
200
248
|
ws = db.workspace_by_tmux_session(session)
|
|
201
249
|
if ws is None:
|
|
@@ -240,10 +288,13 @@ def sidebar_follow(db: DB, session: str) -> None:
|
|
|
240
288
|
if tmux.pane_window(sidebar) == window:
|
|
241
289
|
_repaint(sidebar)
|
|
242
290
|
return
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
291
|
+
# Never take it out of another session someone still looks at: the
|
|
292
|
+
# person's own switch-client leaves that session first, so it reads
|
|
293
|
+
# unattached by now. A second client (the app's, attached to a
|
|
294
|
+
# worker's session) changing windows must not pull it from their terminal.
|
|
295
|
+
home = _home_session(db, root_id, sidebar)
|
|
296
|
+
if home and home != session and tmux.session_attached(home):
|
|
297
|
+
return
|
|
247
298
|
target_pane = tmux.agent_pane_in_window(window, sidebar)
|
|
248
299
|
if not target_pane:
|
|
249
300
|
return
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|