@team-agent/installer 0.1.11 → 0.2.0

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 (110) 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 +135 -0
  10. package/src/team_agent/cli/commands.py +335 -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 +470 -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 +319 -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/start.py +360 -0
  40. package/src/team_agent/mcp_server/__init__.py +42 -0
  41. package/src/team_agent/mcp_server/__main__.py +7 -0
  42. package/src/team_agent/mcp_server/contracts.py +148 -0
  43. package/src/team_agent/mcp_server/normalize.py +257 -0
  44. package/src/team_agent/mcp_server/server.py +150 -0
  45. package/src/team_agent/mcp_server/tools.py +205 -0
  46. package/src/team_agent/message_store/__init__.py +23 -0
  47. package/src/team_agent/message_store/agent_health.py +109 -0
  48. package/src/team_agent/{message_store.py → message_store/core.py} +188 -245
  49. package/src/team_agent/message_store/result_watchers.py +102 -0
  50. package/src/team_agent/message_store/schema.py +266 -0
  51. package/src/team_agent/messaging/__init__.py +1 -0
  52. package/src/team_agent/messaging/activity_detector.py +190 -0
  53. package/src/team_agent/messaging/delivery.py +128 -0
  54. package/src/team_agent/messaging/deps.py +263 -0
  55. package/src/team_agent/messaging/idle_alerts.py +217 -0
  56. package/src/team_agent/messaging/internal_delivery.py +46 -0
  57. package/src/team_agent/messaging/leader.py +317 -0
  58. package/src/team_agent/messaging/leader_panes.py +343 -0
  59. package/src/team_agent/messaging/result_delivery.py +300 -0
  60. package/src/team_agent/messaging/results.py +456 -0
  61. package/src/team_agent/messaging/scheduler.py +418 -0
  62. package/src/team_agent/messaging/send.py +493 -0
  63. package/src/team_agent/messaging/tmux_io.py +337 -0
  64. package/src/team_agent/messaging/tmux_prompt.py +229 -0
  65. package/src/team_agent/orchestrator/__init__.py +376 -0
  66. package/src/team_agent/orchestrator/plan.py +122 -0
  67. package/src/team_agent/orchestrator/state.py +128 -0
  68. package/src/team_agent/profiles/__init__.py +82 -0
  69. package/src/team_agent/profiles/constants.py +19 -0
  70. package/src/team_agent/profiles/core.py +407 -0
  71. package/src/team_agent/profiles/helpers.py +69 -0
  72. package/src/team_agent/profiles/provider_env.py +188 -0
  73. package/src/team_agent/profiles/smoke.py +201 -0
  74. package/src/team_agent/provider_cli/__init__.py +43 -0
  75. package/src/team_agent/provider_cli/adapter.py +167 -0
  76. package/src/team_agent/provider_cli/base.py +48 -0
  77. package/src/team_agent/provider_cli/claude.py +457 -0
  78. package/src/team_agent/provider_cli/codex.py +319 -0
  79. package/src/team_agent/provider_cli/copilot.py +8 -0
  80. package/src/team_agent/provider_cli/fake.py +39 -0
  81. package/src/team_agent/provider_cli/gemini.py +95 -0
  82. package/src/team_agent/provider_cli/opencode.py +8 -0
  83. package/src/team_agent/provider_cli/prompt.py +62 -0
  84. package/src/team_agent/provider_cli/registry.py +18 -0
  85. package/src/team_agent/provider_cli/unsupported.py +32 -0
  86. package/src/team_agent/providers.py +67 -949
  87. package/src/team_agent/quality_gates.py +104 -0
  88. package/src/team_agent/restart/__init__.py +34 -0
  89. package/src/team_agent/restart/orchestration.py +328 -0
  90. package/src/team_agent/restart/selection.py +89 -0
  91. package/src/team_agent/restart/snapshot.py +70 -0
  92. package/src/team_agent/runtime.py +802 -5893
  93. package/src/team_agent/rust_core.py +22 -5
  94. package/src/team_agent/sessions/__init__.py +25 -0
  95. package/src/team_agent/sessions/capture.py +93 -0
  96. package/src/team_agent/sessions/inventory.py +44 -0
  97. package/src/team_agent/sessions/resume.py +135 -0
  98. package/src/team_agent/spec.py +3 -1
  99. package/src/team_agent/state.py +204 -4
  100. package/src/team_agent/status/__init__.py +63 -0
  101. package/src/team_agent/status/approvals.py +52 -0
  102. package/src/team_agent/status/compact.py +158 -0
  103. package/src/team_agent/status/constants.py +18 -0
  104. package/src/team_agent/status/inbox.py +28 -0
  105. package/src/team_agent/status/peek.py +117 -0
  106. package/src/team_agent/status/queries.py +168 -0
  107. package/src/team_agent/terminal.py +57 -0
  108. package/src/team_agent/cli.py +0 -858
  109. package/src/team_agent/mcp_server.py +0 -579
  110. package/src/team_agent/profiles.py +0 -882
@@ -0,0 +1,337 @@
1
+ from __future__ import annotations
2
+
3
+ from team_agent.messaging.deps import (
4
+ TMUX_PASTE_BYTES_PER_SECOND,
5
+ TMUX_PASTE_MAX_READY_TIMEOUT,
6
+ TMUX_PASTE_MIN_READY_TIMEOUT,
7
+ TMUX_STDIN_BUFFER_THRESHOLD,
8
+ TMUX_SUBMIT_BYTES_PER_SECOND,
9
+ TMUX_SUBMIT_MAX_SETTLE_TIMEOUT,
10
+ TMUX_SUBMIT_MIN_SETTLE_TIMEOUT,
11
+ _capture_tmux_pane_text,
12
+ _tmux_load_buffer_stdin as _runtime_tmux_load_buffer_stdin,
13
+ _submit_worker_prompt,
14
+ _wait_for_message_ready,
15
+ re,
16
+ run_cmd,
17
+ subprocess,
18
+ time,
19
+ )
20
+
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ def _tmux_inject_text(
25
+ target: str,
26
+ text: str,
27
+ submit_key: str,
28
+ buffer_name: str,
29
+ attempts: int = 3,
30
+ provider: str = "fake",
31
+ ) -> dict[str, Any]:
32
+ token_match = re.search(r"\[team-agent-token:([^\]]+)\]", text)
33
+ token = token_match.group(1) if token_match else ""
34
+ attempt_log: list[dict[str, Any]] = []
35
+ last_verification = "not_checked"
36
+ ready_timeout = _tmux_paste_ready_timeout(text)
37
+ submit_settle_timeout = _tmux_submit_settle_timeout(text)
38
+ text_bytes = _tmux_text_size(text)
39
+ for attempt in range(1, max(attempts, 1) + 1):
40
+ prepared = _prepare_tmux_pane_for_input(target)
41
+ if not prepared["ok"]:
42
+ attempt_log.append({"attempt": attempt, "visible": False, "verification": prepared["verification"]})
43
+ return {
44
+ "ok": False,
45
+ "stage": prepared["stage"],
46
+ "error": prepared.get("error"),
47
+ "attempts": attempt_log,
48
+ "verification": prepared["verification"],
49
+ }
50
+ baseline = _capture_tmux_pane_text(target)
51
+ if not baseline["ok"]:
52
+ return {
53
+ "ok": False,
54
+ "stage": "pre-paste-capture",
55
+ "error": baseline.get("error"),
56
+ "attempts": attempt_log,
57
+ "verification": "pre_paste_capture_failed",
58
+ }
59
+ baseline_capture = baseline["capture"]
60
+ buffered = _tmux_set_buffer_text(buffer_name, text)
61
+ if not buffered["ok"]:
62
+ return {"ok": False, "stage": buffered["stage"], "error": buffered.get("error"), "attempts": attempt_log}
63
+ proc = run_cmd(["tmux", "paste-buffer", "-t", target, "-b", buffer_name, "-p"], timeout=10)
64
+ deleted = _tmux_delete_buffer(buffer_name)
65
+ if proc.returncode != 0:
66
+ return {
67
+ "ok": False,
68
+ "stage": "paste-buffer",
69
+ "error": proc.stderr.strip(),
70
+ "attempts": attempt_log,
71
+ "buffer_deleted": deleted.get("ok"),
72
+ "buffer_delete_error": deleted.get("error"),
73
+ }
74
+ time.sleep(0.25)
75
+ if token:
76
+ visible, verification, capture_text = _wait_for_message_ready(
77
+ target,
78
+ token,
79
+ ready_timeout,
80
+ expected_text=text,
81
+ baseline_capture=baseline_capture,
82
+ )
83
+ else:
84
+ visible, verification, capture_text = True, "no_token", ""
85
+ last_verification = verification
86
+ attempt_entry = {
87
+ "attempt": attempt,
88
+ "visible": visible,
89
+ "verification": verification,
90
+ "buffer_method": buffered.get("method"),
91
+ "buffer_name": buffer_name,
92
+ "buffer_deleted": deleted.get("ok"),
93
+ "text_bytes": buffered.get("text_bytes"),
94
+ "ready_timeout_sec": ready_timeout,
95
+ }
96
+ if deleted.get("error"):
97
+ attempt_entry["buffer_delete_error"] = deleted.get("error")
98
+ if prepared.get("recovered_from_mode"):
99
+ attempt_entry["recovered_from_mode"] = True
100
+ attempt_log.append(attempt_entry)
101
+ if not visible:
102
+ time.sleep(0.2)
103
+ continue
104
+ submit = _submit_worker_prompt(
105
+ target,
106
+ capture_text,
107
+ submit_key=submit_key,
108
+ settle_timeout=submit_settle_timeout,
109
+ )
110
+ if not submit["ok"]:
111
+ return {
112
+ "ok": False,
113
+ "stage": submit.get("stage", "submit"),
114
+ "error": submit.get("error"),
115
+ "attempts": attempt_log,
116
+ "verification": verification,
117
+ "submit_verification": submit.get("verification"),
118
+ "submit_attempts": submit.get("attempts"),
119
+ }
120
+ submit_verification = _leader_submit_verification(submit.get("verification"), verification, submit_key)
121
+ turn_visible, turn_verification, turn_capture = _wait_for_leader_new_turn(
122
+ target,
123
+ text,
124
+ token,
125
+ provider=provider,
126
+ timeout=2.0,
127
+ )
128
+ if not turn_visible:
129
+ return {
130
+ "ok": False,
131
+ "stage": "turn-boundary-verification",
132
+ "error": f"leader turn boundary not verified: {turn_verification}",
133
+ "attempts": attempt_log,
134
+ "verification": verification,
135
+ "submit_verification": submit_verification,
136
+ "turn_verification": turn_verification,
137
+ "submit_attempts": submit.get("attempts"),
138
+ }
139
+ return {
140
+ "ok": True,
141
+ "stage": "submitted",
142
+ "visible": True,
143
+ "submitted": True,
144
+ "verification": verification,
145
+ "submit_verification": submit_verification,
146
+ "turn_verification": turn_verification,
147
+ "attempts": attempt_log,
148
+ "submit_attempts": submit.get("attempts"),
149
+ }
150
+ return {
151
+ "ok": False,
152
+ "stage": "visible-check",
153
+ "error": f"visible token not found after {max(attempts, 1)} attempts: {last_verification}",
154
+ "attempts": attempt_log,
155
+ "verification": last_verification,
156
+ }
157
+
158
+
159
+ def _leader_submit_verification(submit_verification: str | None, verification: str, submit_key: str) -> str | None:
160
+ if submit_verification != "enter_sent_without_placeholder_check":
161
+ return submit_verification
162
+ if verification == "capture_contains_token":
163
+ return f"{submit_key}_sent_after_visible_token"
164
+ if verification == "capture_contains_message_fragment":
165
+ return f"{submit_key}_sent_after_visible_fragment"
166
+ return submit_verification
167
+
168
+
169
+ def _wait_for_leader_new_turn(
170
+ target: str,
171
+ expected_text: str,
172
+ token: str,
173
+ provider: str,
174
+ timeout: float,
175
+ ) -> tuple[bool, str, str]:
176
+ deadline = time.monotonic() + max(timeout, 0.0)
177
+ last = "not_checked"
178
+ last_capture = ""
179
+ while True:
180
+ capture = _capture_tmux_pane_text(target)
181
+ if capture["ok"]:
182
+ capture_text = capture["capture"]
183
+ last_capture = capture_text
184
+ if _capture_has_leader_new_turn(capture_text, expected_text, token, provider):
185
+ return True, "leader_new_turn_boundary_verified", capture_text
186
+ last = "leader_new_turn_boundary_missing"
187
+ else:
188
+ last = f"capture_failed: {capture.get('error')}"
189
+ if time.monotonic() >= deadline:
190
+ return False, last, last_capture
191
+ time.sleep(0.1)
192
+
193
+
194
+ def _capture_has_leader_new_turn(capture_text: str, expected_text: str, token: str, provider: str) -> bool:
195
+ if provider == "fake":
196
+ return True
197
+ lines = capture_text.splitlines()
198
+ marker_indexes = [index for index, line in enumerate(lines) if re.match(r"^\s*[❯›>]\s*", line)]
199
+ for index in marker_indexes:
200
+ window = "\n".join(lines[index : index + 12])
201
+ if token and token in window:
202
+ return True
203
+ if _leader_turn_contains_message_fragment(window, expected_text):
204
+ return True
205
+ return False
206
+
207
+
208
+ def _leader_turn_contains_message_fragment(capture_text: str, expected_text: str) -> bool:
209
+ haystack = re.sub(r"\s+", "", capture_text)
210
+ for line in expected_text.splitlines():
211
+ compact = re.sub(r"\s+", "", re.sub(r"\[team-agent-token:[^\]]+\]", "", line))
212
+ if len(compact) >= 18 and compact in haystack:
213
+ return True
214
+ return False
215
+
216
+
217
+ def _tmux_text_size(text: str) -> int:
218
+ return len(text.encode("utf-8"))
219
+
220
+
221
+ def _tmux_paste_ready_timeout(text: str) -> float:
222
+ size = _tmux_text_size(text)
223
+ return min(
224
+ TMUX_PASTE_MAX_READY_TIMEOUT,
225
+ max(TMUX_PASTE_MIN_READY_TIMEOUT, size / TMUX_PASTE_BYTES_PER_SECOND),
226
+ )
227
+
228
+
229
+ def _tmux_submit_settle_timeout(text: str) -> float:
230
+ size = _tmux_text_size(text)
231
+ return min(
232
+ TMUX_SUBMIT_MAX_SETTLE_TIMEOUT,
233
+ max(TMUX_SUBMIT_MIN_SETTLE_TIMEOUT, size / TMUX_SUBMIT_BYTES_PER_SECOND),
234
+ )
235
+
236
+
237
+ def _tmux_set_buffer_text(buffer_name: str, text: str) -> dict[str, Any]:
238
+ size = _tmux_text_size(text)
239
+ if size >= TMUX_STDIN_BUFFER_THRESHOLD:
240
+ proc = _runtime_tmux_load_buffer_stdin(buffer_name, text)
241
+ return {
242
+ "ok": proc.returncode == 0,
243
+ "stage": "load-buffer",
244
+ "method": "stdin_load_buffer",
245
+ "text_bytes": size,
246
+ "error": proc.stderr.strip() if proc.returncode != 0 else None,
247
+ }
248
+ proc = run_cmd(["tmux", "set-buffer", "-b", buffer_name, text], timeout=10)
249
+ return {
250
+ "ok": proc.returncode == 0,
251
+ "stage": "set-buffer",
252
+ "method": "set_buffer_arg",
253
+ "text_bytes": size,
254
+ "error": proc.stderr.strip() if proc.returncode != 0 else None,
255
+ }
256
+
257
+
258
+ def _tmux_delete_buffer(buffer_name: str) -> dict[str, Any]:
259
+ proc = run_cmd(["tmux", "delete-buffer", "-b", buffer_name], timeout=10)
260
+ return {
261
+ "ok": proc.returncode == 0,
262
+ "stage": "delete-buffer",
263
+ "error": proc.stderr.strip() if proc.returncode != 0 else None,
264
+ }
265
+
266
+
267
+ def _tmux_load_buffer_stdin(buffer_name: str, text: str) -> subprocess.CompletedProcess[str]:
268
+ return subprocess.run(
269
+ ["tmux", "load-buffer", "-b", buffer_name, "-"],
270
+ input=text,
271
+ text=True,
272
+ capture_output=True,
273
+ timeout=10,
274
+ check=False,
275
+ )
276
+
277
+
278
+ def _prepare_tmux_pane_for_input(target: str) -> dict[str, Any]:
279
+ mode = run_cmd(["tmux", "display-message", "-p", "-t", target, "#{pane_in_mode}"], timeout=5)
280
+ if mode.returncode != 0:
281
+ return {
282
+ "ok": False,
283
+ "stage": "pane-mode-check",
284
+ "verification": "pane_mode_check_failed",
285
+ "error": mode.stderr.strip() or "tmux pane mode check failed",
286
+ }
287
+ if mode.stdout.strip() != "1":
288
+ return {"ok": True, "verification": "pane_input_ready"}
289
+ cancel = run_cmd(["tmux", "send-keys", "-t", target, "-X", "cancel"], timeout=10)
290
+ if cancel.returncode != 0:
291
+ return {
292
+ "ok": False,
293
+ "stage": "pane-mode-cancel",
294
+ "verification": "pane_mode_cancel_failed",
295
+ "error": cancel.stderr.strip() or "tmux copy-mode cancel failed",
296
+ }
297
+ deadline = time.monotonic() + 1.5
298
+ while True:
299
+ check = run_cmd(["tmux", "display-message", "-p", "-t", target, "#{pane_in_mode}"], timeout=5)
300
+ if check.returncode != 0:
301
+ return {
302
+ "ok": False,
303
+ "stage": "pane-mode-check",
304
+ "verification": "pane_mode_recheck_failed",
305
+ "error": check.stderr.strip() or "tmux pane mode recheck failed",
306
+ }
307
+ if check.stdout.strip() != "1":
308
+ return {"ok": True, "verification": "pane_input_ready_after_mode_cancel", "recovered_from_mode": True}
309
+ if time.monotonic() >= deadline:
310
+ return {
311
+ "ok": False,
312
+ "stage": "pane-mode-cancel",
313
+ "verification": "pane_mode_still_active_after_cancel",
314
+ "error": "tmux pane stayed in copy-mode after cancel",
315
+ }
316
+ time.sleep(0.1)
317
+
318
+
319
+
320
+
321
+
322
+
323
+
324
+
325
+
326
+
327
+
328
+
329
+
330
+
331
+
332
+
333
+
334
+
335
+
336
+
337
+
@@ -0,0 +1,229 @@
1
+ from __future__ import annotations
2
+
3
+ from team_agent.messaging.deps import (
4
+ DELIVERY_CAPTURE_LINES,
5
+ PASTED_CONTENT_PROMPT_RE,
6
+ TMUX_SUBMIT_MIN_SETTLE_TIMEOUT,
7
+ re,
8
+ run_cmd,
9
+ time,
10
+ )
11
+
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ def _enable_codex_fast_mode(session_name: str, window_name: str) -> dict[str, Any]:
16
+ target = f"{session_name}:{window_name}"
17
+ proc = run_cmd(["tmux", "send-keys", "-t", target, "/fast", "Enter"], timeout=10)
18
+ if proc.returncode != 0:
19
+ return {"ok": False, "error": proc.stderr.strip() or "tmux send-keys failed"}
20
+ return {"ok": True, "target": target}
21
+
22
+
23
+ def _wait_for_visible_token(target: str, token: str, timeout: float) -> tuple[bool, str]:
24
+ deadline = time.monotonic() + max(timeout, 0.0)
25
+ last = "not_checked"
26
+ while True:
27
+ capture = _capture_tmux_pane_text(target)
28
+ if capture["ok"]:
29
+ if token in capture["capture"] or f"[team-agent-token:{token}]" in capture["capture"]:
30
+ return True, "capture_contains_token"
31
+ last = "capture_missing_token"
32
+ else:
33
+ last = f"capture_failed: {capture.get('error')}"
34
+ if time.monotonic() >= deadline:
35
+ return False, last
36
+ time.sleep(0.1)
37
+
38
+
39
+ def _capture_tmux_pane_text(target: str) -> dict[str, Any]:
40
+ capture = run_cmd(["tmux", "capture-pane", "-p", "-S", f"-{DELIVERY_CAPTURE_LINES}", "-t", target], timeout=5)
41
+ if capture.returncode != 0:
42
+ return {"ok": False, "capture": "", "error": capture.stderr.strip() or "tmux capture-pane failed"}
43
+ return {"ok": True, "capture": capture.stdout}
44
+
45
+
46
+ def _wait_for_message_ready(
47
+ target: str,
48
+ message_id: str,
49
+ timeout: float,
50
+ expected_text: str = "",
51
+ allow_pasted_prompt: bool = True,
52
+ baseline_capture: str = "",
53
+ ) -> tuple[bool, str, str]:
54
+ deadline = time.monotonic() + max(timeout, 0.0)
55
+ last = "not_checked"
56
+ last_capture = ""
57
+ baseline_had_pasted_prompt = _capture_has_pasted_content_prompt(baseline_capture)
58
+ while True:
59
+ capture = _capture_tmux_pane_text(target)
60
+ if capture["ok"]:
61
+ capture_text = capture["capture"]
62
+ last_capture = capture_text
63
+ if message_id in capture_text or f"[team-agent-token:{message_id}]" in capture_text:
64
+ return True, "capture_contains_token", capture_text
65
+ if expected_text and _capture_contains_message_fragment(capture_text, expected_text):
66
+ return True, "capture_contains_message_fragment", capture_text
67
+ if allow_pasted_prompt and _capture_has_pasted_content_prompt(capture_text) and not baseline_had_pasted_prompt:
68
+ return True, "capture_contains_new_pasted_content_prompt", capture_text
69
+ last = "capture_missing_token"
70
+ else:
71
+ last = f"capture_failed: {capture.get('error')}"
72
+ if time.monotonic() >= deadline:
73
+ return False, last, last_capture
74
+ time.sleep(0.1)
75
+
76
+
77
+ def _wait_for_worker_message_ready(target: str, message_id: str, timeout: float, expected_text: str = "") -> tuple[bool, str, str]:
78
+ return _wait_for_message_ready(target, message_id, timeout, expected_text=expected_text)
79
+
80
+
81
+ def _capture_has_pasted_content_prompt(text: str) -> bool:
82
+ lines = [line.rstrip() for line in text.splitlines() if line.strip()]
83
+ if not lines:
84
+ return False
85
+ tail = [line.strip() for line in lines[-12:]]
86
+ tail_text = " ".join(tail)
87
+ if not PASTED_CONTENT_PROMPT_RE.search(tail_text):
88
+ return False
89
+ prompt_markers = ("›", "❯", ">")
90
+ if PASTED_CONTENT_PROMPT_RE.search(tail[-1]):
91
+ return True
92
+ if tail[-1].endswith(("chars]", "line]", "lines]")):
93
+ return True
94
+ if any(line.startswith(prompt_markers) for line in tail):
95
+ return True
96
+ if re.search(r"\b(codex|claude)\s*[>›❯]", tail_text, re.IGNORECASE):
97
+ return True
98
+ return False
99
+
100
+
101
+ def _capture_contains_message_fragment(capture_text: str, expected_text: str) -> bool:
102
+ haystack = _compact_visible_text(capture_text)
103
+ if not haystack:
104
+ return False
105
+ fragments = _message_fragment_candidates(expected_text)
106
+ if not fragments:
107
+ return False
108
+ return any(fragment in haystack for fragment in fragments)
109
+
110
+
111
+ def _message_fragment_candidates(text: str) -> list[str]:
112
+ sanitized = re.sub(r"\[team-agent-token:[^\]]+\]", "", text)
113
+ fragments: list[str] = []
114
+ for line in _message_content_lines(sanitized):
115
+ compact = _compact_visible_text(line)
116
+ if not _is_strong_message_fragment(compact):
117
+ continue
118
+ if len(compact) <= 72:
119
+ fragments.append(compact)
120
+ continue
121
+ midpoint = len(compact) // 2
122
+ fragments.extend(
123
+ [
124
+ compact[:36],
125
+ compact[max(0, midpoint - 18) : midpoint + 18],
126
+ compact[-36:],
127
+ ]
128
+ )
129
+ unique: list[str] = []
130
+ seen: set[str] = set()
131
+ for fragment in fragments:
132
+ if fragment in seen:
133
+ continue
134
+ seen.add(fragment)
135
+ unique.append(fragment)
136
+ return unique
137
+
138
+
139
+ def _message_content_lines(text: str) -> list[str]:
140
+ lines = text.splitlines()
141
+ if lines and lines[0].strip().startswith("Team Agent message from "):
142
+ lines = lines[1:]
143
+ return [line for line in lines if line.strip()]
144
+
145
+
146
+ def _is_strong_message_fragment(compact: str) -> bool:
147
+ if not compact:
148
+ return False
149
+ generic_prefixes = (
150
+ "TeamAgentmessagefrom",
151
+ "TeamAgentpeermessagefrom",
152
+ "TeamAgentstoredthisresult",
153
+ "TeamAgenthascollectedthisresult",
154
+ "Nomanualpolling",
155
+ )
156
+ if compact.startswith(generic_prefixes):
157
+ return False
158
+ if re.fullmatch(r"[-::>›❯]+", compact):
159
+ return False
160
+ if re.search(r"(msg|res)_[0-9A-Fa-f]{8,}", compact):
161
+ return True
162
+ cjk_count = len(re.findall(r"[\u4e00-\u9fff]", compact))
163
+ if cjk_count >= 4 and len(compact) >= 6:
164
+ return True
165
+ return len(compact) >= 18
166
+
167
+
168
+ def _compact_visible_text(text: str) -> str:
169
+ return re.sub(r"\s+", "", text)
170
+
171
+
172
+ def _submit_worker_prompt(
173
+ target: str,
174
+ before_capture: str,
175
+ submit_key: str = "Enter",
176
+ attempts: int = 3,
177
+ settle_timeout: float = TMUX_SUBMIT_MIN_SETTLE_TIMEOUT,
178
+ ) -> dict[str, Any]:
179
+ verify_pasted_prompt = _capture_has_pasted_content_prompt(before_capture)
180
+ attempt_log: list[dict[str, Any]] = []
181
+ for attempt in range(1, max(attempts, 1) + 1):
182
+ proc = run_cmd(["tmux", "send-keys", "-t", target, submit_key], timeout=10)
183
+ if proc.returncode != 0:
184
+ return {
185
+ "ok": False,
186
+ "stage": "send-keys",
187
+ "verification": "send_keys_failed",
188
+ "error": proc.stderr.strip(),
189
+ "attempts": attempt_log,
190
+ }
191
+ if not verify_pasted_prompt:
192
+ return {
193
+ "ok": True,
194
+ "stage": "submitted",
195
+ "verification": "enter_sent_without_placeholder_check",
196
+ "attempts": attempt_log + [{"attempt": attempt, "submitted": True, "verification": "not_required"}],
197
+ }
198
+ cleared, verification = _wait_for_pasted_prompt_cleared(target, settle_timeout)
199
+ attempt_log.append({"attempt": attempt, "submitted": True, "verification": verification})
200
+ if cleared:
201
+ return {
202
+ "ok": True,
203
+ "stage": "submitted",
204
+ "verification": "pasted_content_prompt_absent_after_submit",
205
+ "attempts": attempt_log,
206
+ }
207
+ return {
208
+ "ok": False,
209
+ "stage": "submit-verification",
210
+ "verification": "pasted_content_prompt_still_present_after_retries",
211
+ "error": "pasted content prompt still present after Enter retries",
212
+ "attempts": attempt_log,
213
+ }
214
+
215
+
216
+ def _wait_for_pasted_prompt_cleared(target: str, timeout: float) -> tuple[bool, str]:
217
+ polls = max(1, int(max(timeout, 0.0) / 0.1) + 1)
218
+ last = "pasted_content_prompt_still_present"
219
+ for poll in range(polls):
220
+ capture = run_cmd(["tmux", "capture-pane", "-p", "-S", f"-{DELIVERY_CAPTURE_LINES}", "-t", target], timeout=5)
221
+ if capture.returncode != 0:
222
+ last = "capture_failed"
223
+ elif not _capture_has_pasted_content_prompt(capture.stdout):
224
+ return True, "pasted_content_prompt_absent"
225
+ else:
226
+ last = "pasted_content_prompt_still_present"
227
+ if poll < polls - 1:
228
+ time.sleep(0.1)
229
+ return False, last