@team-agent/installer 0.1.11 → 0.2.1

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.
Files changed (113) hide show
  1. package/crates/team-agent-core/src/lib.rs +50 -5
  2. package/package.json +1 -1
  3. package/schemas/team.schema.json +1 -0
  4. package/src/team_agent/approvals/__init__.py +65 -0
  5. package/src/team_agent/approvals/constants.py +6 -0
  6. package/src/team_agent/approvals/parsing.py +176 -0
  7. package/src/team_agent/approvals/runtime_prompts.py +171 -0
  8. package/src/team_agent/approvals/status.py +165 -0
  9. package/src/team_agent/cli/__init__.py +137 -0
  10. package/src/team_agent/cli/commands.py +339 -0
  11. package/src/team_agent/cli/e2e.py +202 -0
  12. package/src/team_agent/cli/helpers.py +137 -0
  13. package/src/team_agent/cli/parser.py +477 -0
  14. package/src/team_agent/compiler.py +98 -33
  15. package/src/team_agent/coordinator/__init__.py +53 -0
  16. package/src/team_agent/{coordinator.py → coordinator/__main__.py} +3 -1
  17. package/src/team_agent/coordinator/lifecycle.py +334 -0
  18. package/src/team_agent/coordinator/metadata.py +61 -0
  19. package/src/team_agent/coordinator/paths.py +17 -0
  20. package/src/team_agent/diagnose/__init__.py +48 -0
  21. package/src/team_agent/diagnose/checks.py +101 -0
  22. package/src/team_agent/diagnose/health.py +241 -0
  23. package/src/team_agent/diagnose/preflight.py +194 -0
  24. package/src/team_agent/diagnose/quick_start.py +233 -0
  25. package/src/team_agent/display/__init__.py +61 -0
  26. package/src/team_agent/display/close.py +147 -0
  27. package/src/team_agent/display/ghostty.py +77 -0
  28. package/src/team_agent/display/worker_window.py +110 -0
  29. package/src/team_agent/display/workspace.py +473 -0
  30. package/src/team_agent/launch/__init__.py +41 -0
  31. package/src/team_agent/launch/bootstrap.py +85 -0
  32. package/src/team_agent/launch/config.py +106 -0
  33. package/src/team_agent/launch/core.py +291 -0
  34. package/src/team_agent/launch/requirements.py +57 -0
  35. package/src/team_agent/leader/__init__.py +320 -0
  36. package/src/team_agent/lifecycle/__init__.py +5 -0
  37. package/src/team_agent/lifecycle/agents.py +226 -0
  38. package/src/team_agent/lifecycle/operations.py +321 -0
  39. package/src/team_agent/lifecycle/paste_buffer_hygiene.py +39 -0
  40. package/src/team_agent/lifecycle/start.py +363 -0
  41. package/src/team_agent/mcp_server/__init__.py +42 -0
  42. package/src/team_agent/mcp_server/__main__.py +7 -0
  43. package/src/team_agent/mcp_server/contracts.py +148 -0
  44. package/src/team_agent/mcp_server/normalize.py +257 -0
  45. package/src/team_agent/mcp_server/server.py +150 -0
  46. package/src/team_agent/mcp_server/tools.py +205 -0
  47. package/src/team_agent/message_store/__init__.py +23 -0
  48. package/src/team_agent/message_store/agent_health.py +109 -0
  49. package/src/team_agent/{message_store.py → message_store/core.py} +188 -245
  50. package/src/team_agent/message_store/result_watchers.py +102 -0
  51. package/src/team_agent/message_store/schema.py +266 -0
  52. package/src/team_agent/messaging/__init__.py +1 -0
  53. package/src/team_agent/messaging/activity_detector.py +190 -0
  54. package/src/team_agent/messaging/delivery.py +138 -0
  55. package/src/team_agent/messaging/deps.py +263 -0
  56. package/src/team_agent/messaging/idle_alerts.py +323 -0
  57. package/src/team_agent/messaging/internal_delivery.py +46 -0
  58. package/src/team_agent/messaging/leader.py +317 -0
  59. package/src/team_agent/messaging/leader_panes.py +343 -0
  60. package/src/team_agent/messaging/owner_bypass.py +29 -0
  61. package/src/team_agent/messaging/result_delivery.py +300 -0
  62. package/src/team_agent/messaging/results.py +456 -0
  63. package/src/team_agent/messaging/scheduler.py +428 -0
  64. package/src/team_agent/messaging/send.py +500 -0
  65. package/src/team_agent/messaging/session_drift.py +94 -0
  66. package/src/team_agent/messaging/tmux_io.py +337 -0
  67. package/src/team_agent/messaging/tmux_prompt.py +229 -0
  68. package/src/team_agent/orchestrator/__init__.py +376 -0
  69. package/src/team_agent/orchestrator/plan.py +122 -0
  70. package/src/team_agent/orchestrator/state.py +128 -0
  71. package/src/team_agent/profiles/__init__.py +82 -0
  72. package/src/team_agent/profiles/constants.py +19 -0
  73. package/src/team_agent/profiles/core.py +407 -0
  74. package/src/team_agent/profiles/helpers.py +69 -0
  75. package/src/team_agent/profiles/provider_env.py +188 -0
  76. package/src/team_agent/profiles/smoke.py +201 -0
  77. package/src/team_agent/provider_cli/__init__.py +43 -0
  78. package/src/team_agent/provider_cli/adapter.py +167 -0
  79. package/src/team_agent/provider_cli/base.py +48 -0
  80. package/src/team_agent/provider_cli/claude.py +457 -0
  81. package/src/team_agent/provider_cli/codex.py +319 -0
  82. package/src/team_agent/provider_cli/copilot.py +8 -0
  83. package/src/team_agent/provider_cli/fake.py +39 -0
  84. package/src/team_agent/provider_cli/gemini.py +95 -0
  85. package/src/team_agent/provider_cli/opencode.py +8 -0
  86. package/src/team_agent/provider_cli/prompt.py +62 -0
  87. package/src/team_agent/provider_cli/registry.py +18 -0
  88. package/src/team_agent/provider_cli/unsupported.py +32 -0
  89. package/src/team_agent/providers.py +67 -949
  90. package/src/team_agent/quality_gates.py +104 -0
  91. package/src/team_agent/restart/__init__.py +34 -0
  92. package/src/team_agent/restart/orchestration.py +328 -0
  93. package/src/team_agent/restart/selection.py +89 -0
  94. package/src/team_agent/restart/snapshot.py +70 -0
  95. package/src/team_agent/runtime.py +809 -5892
  96. package/src/team_agent/rust_core.py +22 -5
  97. package/src/team_agent/sessions/__init__.py +25 -0
  98. package/src/team_agent/sessions/capture.py +93 -0
  99. package/src/team_agent/sessions/inventory.py +44 -0
  100. package/src/team_agent/sessions/resume.py +135 -0
  101. package/src/team_agent/spec.py +3 -1
  102. package/src/team_agent/state.py +218 -4
  103. package/src/team_agent/status/__init__.py +63 -0
  104. package/src/team_agent/status/approvals.py +52 -0
  105. package/src/team_agent/status/compact.py +158 -0
  106. package/src/team_agent/status/constants.py +18 -0
  107. package/src/team_agent/status/inbox.py +28 -0
  108. package/src/team_agent/status/peek.py +117 -0
  109. package/src/team_agent/status/queries.py +168 -0
  110. package/src/team_agent/terminal.py +57 -0
  111. package/src/team_agent/cli.py +0 -858
  112. package/src/team_agent/mcp_server.py +0 -579
  113. package/src/team_agent/profiles.py +0 -882
@@ -0,0 +1,321 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import hashlib
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from team_agent import runtime as _runtime
10
+ from team_agent.errors import RuntimeError
11
+ from team_agent.events import EventLog
12
+ from team_agent.spec import load_spec, validate_spec
13
+ from team_agent.state import (
14
+ SESSION_CAPTURE_FIELDS,
15
+ check_team_owner,
16
+ load_runtime_state,
17
+ resolve_team_scoped_state,
18
+ save_runtime_state,
19
+ save_team_scoped_state,
20
+ write_spec,
21
+ write_team_state,
22
+ )
23
+
24
+
25
+ _RUNTIME_SYMBOLS = (
26
+ "_capture_agent_session",
27
+ "_close_ghostty_display",
28
+ "_close_ghostty_workspace_slot",
29
+ "_effective_runtime_config",
30
+ "_find_agent",
31
+ "_handle_startup_prompts_and_verify_window",
32
+ "_open_ghostty_worker_window",
33
+ "_open_ghostty_workspace_agent_display",
34
+ "_running_agent_state",
35
+ "_runtime_lock",
36
+ "_save_team_runtime_snapshot",
37
+ "_spec_team_dir",
38
+ "_tmux_start_command_for_agent_window",
39
+ "_tmux_window_exists",
40
+ "ensure_workspace_dirs",
41
+ "get_adapter",
42
+ "run_cmd",
43
+ "shell_fork_command_for_agent",
44
+ "start_agent",
45
+ "start_coordinator",
46
+ )
47
+ for _name in _RUNTIME_SYMBOLS:
48
+ if not hasattr(_runtime, _name):
49
+ raise ImportError(f"team_agent.runtime missing lifecycle operation dependency: {_name}")
50
+
51
+
52
+ def _runtime_proxy(name: str):
53
+ def proxy(*args: Any, **kwargs: Any) -> Any:
54
+ return getattr(_runtime, name)(*args, **kwargs)
55
+
56
+ return proxy
57
+
58
+
59
+ globals().update({_name: _runtime_proxy(_name) for _name in _RUNTIME_SYMBOLS})
60
+
61
+
62
+ def stop_agent(workspace: Path, agent_id: str, *, team: str | None = None) -> dict[str, Any]:
63
+ with _runtime_lock(workspace, "stop-agent"):
64
+ state, refusal = resolve_team_scoped_state(workspace, team)
65
+ if refusal:
66
+ return refusal
67
+ gate = check_team_owner(state)
68
+ if gate:
69
+ return gate
70
+ spec_path = Path(state.get("spec_path", workspace / "team.spec.yaml"))
71
+ spec = load_spec(spec_path)
72
+ agent = _find_agent(spec, agent_id)
73
+ if not agent or spec.get("leader", {}).get("id") == agent_id:
74
+ raise RuntimeError(f"unknown worker agent id: {agent_id}")
75
+ ensure_workspace_dirs(workspace)
76
+ event_log = EventLog(workspace)
77
+ session_name = state.get("session_name") or spec.get("runtime", {}).get("session_name") or f"team-{spec['team']['name']}"
78
+ agent_state = dict(state.get("agents", {}).get(agent_id) or {"provider": agent["provider"], "agent_id": agent_id})
79
+ window = str(agent_state.get("window") or agent_id)
80
+ target = f"{session_name}:{window}"
81
+ stopped = False
82
+ if _tmux_window_exists(session_name, window):
83
+ proc = run_cmd(["tmux", "kill-window", "-t", target], timeout=10)
84
+ if proc.returncode != 0:
85
+ event_log.write("stop_agent.window_stop_failed", agent_id=agent_id, target=target, stderr=proc.stderr.strip())
86
+ raise RuntimeError(f"failed to stop agent {agent_id}: {proc.stderr.strip()}")
87
+ stopped = True
88
+ _close_ghostty_display(agent_id, agent_state, event_log)
89
+ display = agent_state.get("display") or {}
90
+ if display.get("backend") == "ghostty_workspace":
91
+ _close_ghostty_workspace_slot(agent_id, display, event_log)
92
+ agent_state["display"] = display
93
+ agent_state.update({"status": "stopped", "provider": agent["provider"], "agent_id": agent_id, "window": window})
94
+ state.setdefault("agents", {})[agent_id] = agent_state
95
+ save_team_scoped_state(workspace, state)
96
+ _save_team_runtime_snapshot(workspace, state)
97
+ state_path = write_team_state(workspace, spec, state)
98
+ event_log.write("stop_agent.complete", agent_id=agent_id, target=target, stopped=stopped)
99
+ return {"ok": True, "agent_id": agent_id, "status": "stopped", "target": target, "stopped": stopped, "state_file": str(state_path)}
100
+
101
+
102
+ def reset_agent(workspace: Path, agent_id: str, *, discard_session: bool = False, open_display: bool = True, team: str | None = None) -> dict[str, Any]:
103
+ if not discard_session:
104
+ return {"ok": False, "agent_id": agent_id, "status": "refused", "reason": "discard_session_required"}
105
+ state, refusal = resolve_team_scoped_state(workspace, team)
106
+ if refusal:
107
+ return refusal
108
+ gate = check_team_owner(state)
109
+ if gate:
110
+ return gate
111
+ stopped = stop_agent(workspace, agent_id, team=team)
112
+ state, refusal = resolve_team_scoped_state(workspace, team)
113
+ if refusal:
114
+ return refusal
115
+ spec_path = Path(state.get("spec_path", workspace / "team.spec.yaml"))
116
+ spec = load_spec(spec_path)
117
+ agent_state = dict(state.get("agents", {}).get(agent_id) or {})
118
+ discarded_session_id = agent_state.get("session_id")
119
+ for key in [*SESSION_CAPTURE_FIELDS, "_pending_session_id"]:
120
+ agent_state.pop(key, None)
121
+ agent_state["status"] = "stopped"
122
+ state.setdefault("agents", {})[agent_id] = agent_state
123
+ EventLog(workspace).write("discard.session_tombstone", agent_id=agent_id, discarded_session_id=discarded_session_id)
124
+ save_team_scoped_state(workspace, state)
125
+ write_team_state(workspace, spec, state)
126
+ started = start_agent(workspace, agent_id, force=True, open_display=open_display, allow_fresh=True, team=team)
127
+ EventLog(workspace).write("reset_agent.complete", agent_id=agent_id, stopped=stopped, started=started)
128
+ return {"ok": True, "agent_id": agent_id, "status": "running", "stopped": stopped, "started": started}
129
+
130
+
131
+ def add_agent(workspace: Path, agent_id: str, *, role_file_path: str, open_display: bool = True, team: str | None = None) -> dict[str, Any]:
132
+ from team_agent.compiler import compile_role_doc_agent
133
+
134
+ state, refusal = resolve_team_scoped_state(workspace, team)
135
+ if refusal:
136
+ return refusal
137
+ gate = check_team_owner(state)
138
+ if gate:
139
+ return gate
140
+ spec_path = Path(state.get("spec_path", workspace / "team.spec.yaml"))
141
+ spec = load_spec(spec_path)
142
+ if _find_agent(spec, agent_id):
143
+ raise RuntimeError(f"agent id already exists: {agent_id}")
144
+ team_dir = Path(str(state.get("team_dir"))) if state.get("team_dir") else _spec_team_dir(spec_path, workspace)
145
+ role_file = Path(role_file_path)
146
+ if not role_file.is_absolute():
147
+ role_file = workspace / role_file
148
+ if not role_file.is_file():
149
+ raise RuntimeError(f"role file not found: {role_file}")
150
+ role_bytes = role_file.read_bytes()
151
+ role_sha = hashlib.sha256(role_bytes).hexdigest()
152
+ dynamic_dir = workspace / ".team" / "dynamic-role-files"
153
+ dynamic_path = dynamic_dir / f"{agent_id}.md"
154
+ old_spec_text = spec_path.read_text(encoding="utf-8")
155
+ old_state = copy.deepcopy(state)
156
+ old_dynamic = dynamic_path.read_bytes() if dynamic_path.exists() else None
157
+ event_log = EventLog(workspace)
158
+ try:
159
+ dynamic_dir.mkdir(parents=True, exist_ok=True)
160
+ dynamic_path.write_bytes(role_bytes)
161
+ agent = compile_role_doc_agent(dynamic_path, team_dir, agent_id)
162
+ spec.setdefault("agents", []).append(agent)
163
+ spec.setdefault("runtime", {}).setdefault("startup_order", []).append(agent_id)
164
+ validate_spec(spec, base_dir=spec_path.parent)
165
+ write_spec(spec_path, spec)
166
+ write_team_state(workspace, spec, state)
167
+ started = start_agent(workspace, agent_id, open_display=open_display, allow_fresh=True, team=team)
168
+ state, _refusal_after = resolve_team_scoped_state(workspace, team)
169
+ state["agents"][agent_id]["dynamic_role_file"] = str(dynamic_path.relative_to(workspace))
170
+ state["agents"][agent_id]["role_file_sha"] = role_sha
171
+ save_team_scoped_state(workspace, state)
172
+ state_path = write_team_state(workspace, spec, state)
173
+ except Exception:
174
+ spec_path.write_text(old_spec_text, encoding="utf-8")
175
+ save_team_scoped_state(workspace, old_state)
176
+ if old_dynamic is None:
177
+ dynamic_path.unlink(missing_ok=True)
178
+ else:
179
+ dynamic_path.parent.mkdir(parents=True, exist_ok=True)
180
+ dynamic_path.write_bytes(old_dynamic)
181
+ raise
182
+ event_log.write("add_agent.complete", agent_id=agent_id, role_file=str(dynamic_path), role_file_sha=role_sha, started=started)
183
+ return {
184
+ "ok": True,
185
+ "agent_id": agent_id,
186
+ "new_agent_id": agent_id,
187
+ "status": "running",
188
+ "role_file": str(dynamic_path),
189
+ "role_file_sha": role_sha,
190
+ "started": started,
191
+ "state_file": str(state_path),
192
+ }
193
+
194
+
195
+ def fork_agent(
196
+ workspace: Path,
197
+ source_agent_id: str,
198
+ *,
199
+ as_agent_id: str,
200
+ label: str | None = None,
201
+ open_display: bool = True,
202
+ team: str | None = None,
203
+ ) -> dict[str, Any]:
204
+ state, refusal = resolve_team_scoped_state(workspace, team)
205
+ if refusal:
206
+ return refusal
207
+ gate = check_team_owner(state)
208
+ if gate:
209
+ return gate
210
+ spec_path = Path(state.get("spec_path", workspace / "team.spec.yaml"))
211
+ spec = load_spec(spec_path)
212
+ if _find_agent(spec, as_agent_id):
213
+ raise RuntimeError(f"agent id already exists: {as_agent_id}")
214
+ source_agent = _find_agent(spec, source_agent_id)
215
+ if not source_agent or spec.get("leader", {}).get("id") == source_agent_id:
216
+ raise RuntimeError(f"unknown worker agent id: {source_agent_id}")
217
+ source_state = state.get("agents", {}).get(source_agent_id) or {}
218
+ source_session_id = str(source_state.get("session_id") or "")
219
+ if not source_session_id:
220
+ raise RuntimeError(f"cannot fork {source_agent_id}: source session_id is missing")
221
+ session_name = state.get("session_name") or spec.get("runtime", {}).get("session_name") or f"team-{spec['team']['name']}"
222
+ if _tmux_window_exists(session_name, as_agent_id):
223
+ raise RuntimeError(f"tmux window already exists for fork target: {session_name}:{as_agent_id}")
224
+ new_agent = copy.deepcopy(source_agent)
225
+ new_agent["id"] = as_agent_id
226
+ new_agent["role"] = str(label or new_agent.get("role") or as_agent_id)
227
+ new_agent["forked_from"] = source_agent_id
228
+ new_agent["preferred_for"] = [as_agent_id, new_agent["role"]]
229
+ old_spec_text = spec_path.read_text(encoding="utf-8")
230
+ old_state = copy.deepcopy(state)
231
+ event_log = EventLog(workspace)
232
+ mcp_path: Path | None = None
233
+ try:
234
+ spec.setdefault("agents", []).append(new_agent)
235
+ spec.setdefault("runtime", {}).setdefault("startup_order", []).append(as_agent_id)
236
+ validate_spec(spec, base_dir=spec_path.parent)
237
+ write_spec(spec_path, spec)
238
+ runtime_cfg = _effective_runtime_config(spec.get("runtime", {}))
239
+ adapter = get_adapter(new_agent["provider"])
240
+ if not adapter.supports_session_fork(new_agent):
241
+ raise RuntimeError(f"{new_agent['provider']} does not support native session fork")
242
+ mcp_config = adapter.mcp_config(workspace, as_agent_id)
243
+ mcp_path = adapter.install_mcp(workspace, as_agent_id, mcp_config)
244
+ command_agent = copy.deepcopy(new_agent)
245
+ command_agent["_runtime"] = runtime_cfg
246
+ command = shell_fork_command_for_agent(command_agent, source_session_id, workspace, mcp_config)
247
+ tmux_cmd, tmux_start_mode = _tmux_start_command_for_agent_window(session_name, as_agent_id, command)
248
+ event_log.write(
249
+ "fork_agent.agent_start",
250
+ source_agent_id=source_agent_id,
251
+ new_agent_id=as_agent_id,
252
+ provider=new_agent["provider"],
253
+ source_session_id=source_session_id,
254
+ tmux_start_mode=tmux_start_mode,
255
+ command=command,
256
+ mcp_config=str(mcp_path),
257
+ )
258
+ proc = run_cmd(tmux_cmd)
259
+ if proc.returncode != 0:
260
+ raise RuntimeError(f"failed to fork agent {source_agent_id}: {proc.stderr.strip()}")
261
+ if not _handle_startup_prompts_and_verify_window(
262
+ adapter, event_log, "fork_agent", as_agent_id, new_agent["provider"], session_name, "forked"
263
+ ):
264
+ raise RuntimeError(f"Failed to fork agent {as_agent_id}: tmux window exited after start")
265
+ spawn_time = datetime.now(timezone.utc)
266
+ agent_state = _running_agent_state(workspace, new_agent, {})
267
+ agent_state.update(
268
+ {
269
+ "mcp_config": str(mcp_path),
270
+ "session_name": session_name,
271
+ "spawned_at": spawn_time.isoformat(),
272
+ "forked_from": source_agent_id,
273
+ }
274
+ )
275
+ if command_agent.get("_session_id"):
276
+ agent_state["_pending_session_id"] = command_agent["_session_id"]
277
+ _capture_agent_session(
278
+ workspace,
279
+ as_agent_id,
280
+ agent_state,
281
+ event_log,
282
+ timeout_s=1.5,
283
+ exclude_session_ids={source_session_id},
284
+ )
285
+ if open_display and state.get("display_backend") in {"ghostty", "ghostty_window"}:
286
+ agent_state["display"] = _open_ghostty_worker_window(workspace, session_name, as_agent_id, new_agent, event_log)
287
+ elif open_display and state.get("display_backend") == "ghostty_workspace":
288
+ agent_state["display"] = _open_ghostty_workspace_agent_display(session_name, as_agent_id, new_agent, {}, event_log)
289
+ state.setdefault("agents", {})[as_agent_id] = agent_state
290
+ save_team_scoped_state(workspace, state)
291
+ _save_team_runtime_snapshot(workspace, state)
292
+ state_path = write_team_state(workspace, spec, state)
293
+ coordinator = start_coordinator(workspace)
294
+ except Exception:
295
+ if _tmux_window_exists(session_name, as_agent_id):
296
+ run_cmd(["tmux", "kill-window", "-t", f"{session_name}:{as_agent_id}"], timeout=10)
297
+ if mcp_path is not None:
298
+ try:
299
+ get_adapter(new_agent["provider"]).cleanup_mcp(workspace, as_agent_id, mcp_path)
300
+ except Exception as exc:
301
+ event_log.write("fork_agent.mcp_cleanup_failed", new_agent_id=as_agent_id, error=str(exc))
302
+ spec_path.write_text(old_spec_text, encoding="utf-8")
303
+ save_team_scoped_state(workspace, old_state)
304
+ raise
305
+ event_log.write(
306
+ "fork_agent.complete",
307
+ source_agent_id=source_agent_id,
308
+ new_agent_id=as_agent_id,
309
+ session_id=state["agents"][as_agent_id].get("session_id"),
310
+ coordinator=coordinator,
311
+ )
312
+ return {
313
+ "ok": True,
314
+ "source_agent_id": source_agent_id,
315
+ "new_agent_id": as_agent_id,
316
+ "agent_id": as_agent_id,
317
+ "status": "running",
318
+ "session_id": state["agents"][as_agent_id].get("session_id"),
319
+ "state_file": str(state_path),
320
+ "coordinator": coordinator,
321
+ }
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from team_agent.events import EventLog
7
+
8
+ _TEAM_AGENT_BUFFER_PREFIXES = ("team-agent-send-", "team-agent-leader-receiver-", "team-agent-")
9
+
10
+
11
+ def _is_team_agent_buffer(name: str) -> bool:
12
+ return any(name.startswith(prefix) for prefix in _TEAM_AGENT_BUFFER_PREFIXES)
13
+
14
+
15
+ def cleanup_stale_team_agent_buffers(workspace: Path, event_log: EventLog, *, context: str) -> dict[str, Any]:
16
+ from team_agent.runtime import run_cmd
17
+ proc = run_cmd(["tmux", "list-buffers", "-F", "#{buffer_name}"], timeout=5)
18
+ if proc.returncode != 0:
19
+ event_log.write("paste_buffer_hygiene.list_failed", context=context, stderr=proc.stderr.strip()[:200])
20
+ return {"ok": False, "deleted": [], "reason": "list_buffers_failed"}
21
+ names = [line.strip() for line in proc.stdout.splitlines() if line.strip()]
22
+ targets = [name for name in names if _is_team_agent_buffer(name)]
23
+ deleted: list[str] = []
24
+ for name in targets:
25
+ delete_proc = run_cmd(["tmux", "delete-buffer", "-b", name], timeout=5)
26
+ if delete_proc.returncode == 0:
27
+ deleted.append(name)
28
+ if deleted:
29
+ event_log.write(
30
+ "paste_buffer_hygiene.prevented_resume_injection",
31
+ context=context,
32
+ deleted_buffers=deleted,
33
+ scanned_count=len(names),
34
+ matched_count=len(targets),
35
+ )
36
+ return {"ok": True, "deleted": deleted, "scanned": len(names), "matched": len(targets)}
37
+
38
+
39
+ __all__ = ["cleanup_stale_team_agent_buffers"]