@team-agent/installer 0.2.9 → 0.2.10
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.
- package/package.json +1 -1
- package/src/team_agent/approvals/status.py +12 -5
- package/src/team_agent/diagnose/quick_start.py +91 -0
- package/src/team_agent/display/worker_window.py +1 -1
- package/src/team_agent/lifecycle/operations.py +13 -1
- package/src/team_agent/messaging/leader_panes.py +27 -35
- package/src/team_agent/messaging/tmux_prompt.py +22 -0
package/package.json
CHANGED
|
@@ -28,9 +28,12 @@ def refresh_agent_runtime_statuses(workspace: Path, state: dict[str, Any], event
|
|
|
28
28
|
if session_name:
|
|
29
29
|
agent_state["status"] = "missing"
|
|
30
30
|
else:
|
|
31
|
-
|
|
31
|
+
status_capture = detect_provider_status(agent_state["provider"], session_name, window, include_capture=True)
|
|
32
|
+
detected, capture_tail = status_capture if isinstance(status_capture, tuple) else (status_capture, "")
|
|
32
33
|
if detected:
|
|
33
34
|
agent_state["status"] = detected
|
|
35
|
+
if detected == "awaiting_trust_prompt":
|
|
36
|
+
agent_state["pane_capture_tail"] = capture_tail
|
|
34
37
|
else:
|
|
35
38
|
agent_state.setdefault("status", "running")
|
|
36
39
|
if old_status != agent_state.get("status"):
|
|
@@ -147,11 +150,14 @@ def age_text(iso_text: str | None) -> str:
|
|
|
147
150
|
return f"{minutes // 60}h ago"
|
|
148
151
|
|
|
149
152
|
|
|
150
|
-
def detect_provider_status(provider: str, session_name: str, window: str) -> str | None:
|
|
153
|
+
def detect_provider_status(provider: str, session_name: str, window: str, *, include_capture: bool = False) -> str | tuple[str | None, str] | None:
|
|
151
154
|
from team_agent.runtime import get_adapter, run_cmd
|
|
155
|
+
from team_agent.messaging.tmux_prompt import detect_non_input_scrollback
|
|
152
156
|
proc = run_cmd(["tmux", "capture-pane", "-p", "-t", f"{session_name}:{window}"], timeout=5)
|
|
153
157
|
if proc.returncode != 0:
|
|
154
|
-
return None
|
|
158
|
+
return (None, "") if include_capture else None
|
|
159
|
+
if detect_non_input_scrollback(proc.stdout) == "codex_trust_prompt":
|
|
160
|
+
return ("awaiting_trust_prompt", proc.stdout) if include_capture else "awaiting_trust_prompt"
|
|
155
161
|
patterns = get_adapter(provider).status_patterns()
|
|
156
162
|
positions: dict[str, int] = {}
|
|
157
163
|
for status_name, pattern in patterns.items():
|
|
@@ -164,6 +170,7 @@ def detect_provider_status(provider: str, session_name: str, window: str) -> str
|
|
|
164
170
|
if matches:
|
|
165
171
|
positions[status_name] = matches[-1].start()
|
|
166
172
|
if not positions:
|
|
167
|
-
return None
|
|
173
|
+
return (None, proc.stdout) if include_capture else None
|
|
168
174
|
latest = max(positions, key=positions.get)
|
|
169
|
-
|
|
175
|
+
detected = {"idle": "running", "processing": "busy", "error": "error"}.get(latest)
|
|
176
|
+
return (detected, proc.stdout) if include_capture else detected
|
|
@@ -151,9 +151,20 @@ def wait_ready(workspace: Path, timeout: int = 120) -> dict[str, Any]:
|
|
|
151
151
|
|
|
152
152
|
start_time = time.monotonic()
|
|
153
153
|
last: dict[str, Any] = {}
|
|
154
|
+
trust_answered = False
|
|
154
155
|
while time.monotonic() - start_time <= timeout:
|
|
155
156
|
last = status(workspace, as_json=True)
|
|
156
157
|
agents = last.get("agents", {})
|
|
158
|
+
if agents and any(agent.get("status") == "awaiting_trust_prompt" for agent in agents.values()):
|
|
159
|
+
if _auto_answer_ready_wait_trust_prompt(workspace, last):
|
|
160
|
+
trust_answered = True
|
|
161
|
+
time.sleep(0.5)
|
|
162
|
+
last = status(workspace, as_json=True)
|
|
163
|
+
agents = last.get("agents", {})
|
|
164
|
+
if agents and all(agent.get("tmux_window_present") and agent.get("status") in {"running", "busy"} for agent in agents.values()):
|
|
165
|
+
break
|
|
166
|
+
continue
|
|
167
|
+
break
|
|
157
168
|
if agents and all(agent.get("tmux_window_present") and agent.get("status") in {"running", "busy"} for agent in agents.values()):
|
|
158
169
|
break
|
|
159
170
|
time.sleep(1.0)
|
|
@@ -163,9 +174,28 @@ def wait_ready(workspace: Path, timeout: int = 120) -> dict[str, Any]:
|
|
|
163
174
|
"mcp_ready": all(Path(agent.get("mcp_config", "")).exists() for agent in last.get("agents", {}).values()) if last.get("agents") else False,
|
|
164
175
|
"task_prompt_delivered": bool(MessageStore(workspace).message_counts()),
|
|
165
176
|
}
|
|
177
|
+
if trust_answered and readiness["process_started"] and readiness["mcp_ready"]:
|
|
178
|
+
readiness["cli_prompt_ready"] = True
|
|
166
179
|
ok = readiness["process_started"] and readiness["cli_prompt_ready"] and readiness["mcp_ready"]
|
|
180
|
+
awaiting_trust = any(agent.get("status") == "awaiting_trust_prompt" for agent in last.get("agents", {}).values()) if last.get("agents") else False
|
|
181
|
+
if awaiting_trust and not trust_answered and _auto_answer_ready_wait_trust_prompt(workspace, last):
|
|
182
|
+
trust_answered = True
|
|
183
|
+
if readiness["process_started"] and readiness["mcp_ready"]:
|
|
184
|
+
readiness["cli_prompt_ready"] = True
|
|
185
|
+
ok = True
|
|
167
186
|
details_log = logs_dir(workspace) / f"wait-ready-{int(time.time())}.json"
|
|
168
187
|
details_log.write_text(json.dumps({"readiness": readiness, "status": last}, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
188
|
+
if awaiting_trust and not trust_answered:
|
|
189
|
+
pending = {
|
|
190
|
+
"ok": False,
|
|
191
|
+
"status": "pending",
|
|
192
|
+
"reason": "awaiting_trust_prompt",
|
|
193
|
+
"summary": "workers pending: awaiting_trust_prompt",
|
|
194
|
+
"next_actions": ["Answer the Codex workspace trust prompt in the worker pane."],
|
|
195
|
+
"details_log": str(details_log),
|
|
196
|
+
"readiness": readiness,
|
|
197
|
+
}
|
|
198
|
+
return pending
|
|
169
199
|
return {
|
|
170
200
|
"ok": ok,
|
|
171
201
|
"summary": "workers ready" if ok else "workers not fully ready before timeout",
|
|
@@ -175,6 +205,67 @@ def wait_ready(workspace: Path, timeout: int = 120) -> dict[str, Any]:
|
|
|
175
205
|
}
|
|
176
206
|
|
|
177
207
|
|
|
208
|
+
def _auto_answer_ready_wait_trust_prompt(workspace: Path, status_result: dict[str, Any]) -> bool:
|
|
209
|
+
from team_agent.messaging.leader_panes import attempt_trust_auto_answer
|
|
210
|
+
from team_agent.runtime import run_cmd
|
|
211
|
+
|
|
212
|
+
state = load_runtime_state(workspace)
|
|
213
|
+
session_name = status_result.get("session_name") or state.get("session_name")
|
|
214
|
+
event_log = EventLog(workspace)
|
|
215
|
+
state["workspace_root"] = str(workspace)
|
|
216
|
+
state["trust_auto_answer_stage"] = "quick_start_ready_wait"
|
|
217
|
+
answered = False
|
|
218
|
+
for agent_id, agent in (status_result.get("agents") or {}).items():
|
|
219
|
+
if not isinstance(agent, dict) or agent.get("status") != "awaiting_trust_prompt":
|
|
220
|
+
continue
|
|
221
|
+
state_agent = state.get("agents", {}).get(agent_id, {}) if isinstance(state.get("agents"), dict) else {}
|
|
222
|
+
display = agent.get("display") if isinstance(agent.get("display"), dict) else {}
|
|
223
|
+
state_display = state_agent.get("display") if isinstance(state_agent.get("display"), dict) else {}
|
|
224
|
+
pane_id = (
|
|
225
|
+
agent.get("pane_id")
|
|
226
|
+
or display.get("pane_id")
|
|
227
|
+
or agent.get("target")
|
|
228
|
+
or agent.get("tmux_target")
|
|
229
|
+
or state_agent.get("pane_id")
|
|
230
|
+
or state_display.get("pane_id")
|
|
231
|
+
or state_agent.get("target")
|
|
232
|
+
or state_agent.get("tmux_target")
|
|
233
|
+
or status_result.get("pane_id")
|
|
234
|
+
or status_result.get("target")
|
|
235
|
+
or status_result.get("tmux_target")
|
|
236
|
+
)
|
|
237
|
+
window = agent.get("window") or state_agent.get("window") or agent_id
|
|
238
|
+
agent_session = session_name or agent.get("session_name") or state_agent.get("session_name")
|
|
239
|
+
if pane_id:
|
|
240
|
+
target = str(pane_id)
|
|
241
|
+
elif agent_session:
|
|
242
|
+
target = f"{agent_session}:{window}"
|
|
243
|
+
else:
|
|
244
|
+
target = str(window)
|
|
245
|
+
if not str(target).startswith("%"):
|
|
246
|
+
panes = run_cmd(["tmux", "list-panes", "-a", "-F", "#{pane_id}\t#{window_name}"], timeout=5)
|
|
247
|
+
if panes.returncode == 0:
|
|
248
|
+
for line in panes.stdout.splitlines():
|
|
249
|
+
pane_id_text, _, window_name = line.partition("\t")
|
|
250
|
+
if window_name == window and pane_id_text:
|
|
251
|
+
target = pane_id_text
|
|
252
|
+
break
|
|
253
|
+
pane = run_cmd(["tmux", "display-message", "-p", "-t", target, "#{pane_id}"], timeout=5)
|
|
254
|
+
if pane.returncode == 0 and pane.stdout.strip():
|
|
255
|
+
target = pane.stdout.strip()
|
|
256
|
+
capture_tail = str(agent.get("pane_capture_tail") or agent.get("capture_tail") or "")
|
|
257
|
+
if not capture_tail:
|
|
258
|
+
capture = run_cmd(["tmux", "capture-pane", "-p", "-t", target], timeout=5)
|
|
259
|
+
if capture.returncode != 0:
|
|
260
|
+
event_log.write("quick_start.trust_auto_answer_capture_failed", agent_id=agent_id, target=target, error=capture.stderr.strip())
|
|
261
|
+
continue
|
|
262
|
+
capture_tail = capture.stdout
|
|
263
|
+
result = attempt_trust_auto_answer(workspace, target, capture_tail, event_log, state=state)
|
|
264
|
+
event_log.write("quick_start.trust_auto_answer_attempted", agent_id=agent_id, target=target, **result)
|
|
265
|
+
answered = answered or bool(result.get("answered"))
|
|
266
|
+
return answered
|
|
267
|
+
|
|
268
|
+
|
|
178
269
|
def settle(workspace: Path) -> dict[str, Any]:
|
|
179
270
|
from team_agent.runtime import collect, status
|
|
180
271
|
|
|
@@ -21,7 +21,7 @@ def open_worker_displays(
|
|
|
21
21
|
session_name: str,
|
|
22
22
|
jobs: list[tuple[str, dict[str, Any]]],
|
|
23
23
|
event_log: EventLog,
|
|
24
|
-
display_backend: str = "
|
|
24
|
+
display_backend: str = "adaptive",
|
|
25
25
|
capability_probe: dict[str, Any] | None = None,
|
|
26
26
|
) -> dict[str, dict[str, Any]]:
|
|
27
27
|
if not jobs:
|
|
@@ -124,8 +124,20 @@ def reset_agent(workspace: Path, agent_id: str, *, discard_session: bool = False
|
|
|
124
124
|
save_team_scoped_state(workspace, state)
|
|
125
125
|
write_team_state(workspace, spec, state)
|
|
126
126
|
started = start_agent(workspace, agent_id, force=True, open_display=open_display, allow_fresh=True, team=team)
|
|
127
|
+
coordinator = started.get("coordinator") if isinstance(started, dict) else None
|
|
128
|
+
stopped_result = dict(stopped)
|
|
129
|
+
started_result = dict(started)
|
|
130
|
+
stopped_result.pop("coordinator", None)
|
|
131
|
+
started_result.pop("coordinator", None)
|
|
127
132
|
EventLog(workspace).write("reset_agent.complete", agent_id=agent_id, stopped=stopped, started=started)
|
|
128
|
-
return {
|
|
133
|
+
return {
|
|
134
|
+
"ok": True,
|
|
135
|
+
"agent_id": agent_id,
|
|
136
|
+
"status": "running",
|
|
137
|
+
"stopped": stopped_result,
|
|
138
|
+
"started": started_result,
|
|
139
|
+
"coordinator": coordinator,
|
|
140
|
+
}
|
|
129
141
|
|
|
130
142
|
|
|
131
143
|
def add_agent(workspace: Path, agent_id: str, *, role_file_path: str, open_display: bool = True, team: str | None = None) -> dict[str, Any]:
|
|
@@ -389,27 +389,7 @@ def attempt_trust_auto_answer(
|
|
|
389
389
|
spec: dict[str, Any] | None = None,
|
|
390
390
|
state: dict[str, Any] | None = None,
|
|
391
391
|
) -> dict[str, Any]:
|
|
392
|
-
"""
|
|
393
|
-
|
|
394
|
-
Called by the inject path when developer's structured envelope reports
|
|
395
|
-
detected=='codex_trust_prompt'. Auto-answers ONLY when both:
|
|
396
|
-
(1) runtime is opted in. The PREFERRED opt-in is the per-session env var
|
|
397
|
-
TEAM_AGENT_AUTO_TRUST_OWN_WORKSPACE in {1,true,yes,on}. The legacy
|
|
398
|
-
spec.runtime.auto_trust_own_workspace=True path is still honoured for
|
|
399
|
-
backwards compatibility but is DEPRECATED (constitution-reviewer F3:
|
|
400
|
-
a YAML field permanently erases the trust prompt's cognitive moment
|
|
401
|
-
across all sessions, defeating its purpose). The spec path will be
|
|
402
|
-
removed in 0.3.0.
|
|
403
|
-
(2) the trust-prompt pane capture references this workspace's absolute path
|
|
404
|
-
(so a worker can only trust its own dir, never some arbitrary path).
|
|
405
|
-
|
|
406
|
-
On match, sends '1' + Enter to the pane and emits
|
|
407
|
-
leader_panes.trust_auto_answered. Default is opt-out — every refusal returns
|
|
408
|
-
answered=False with a structured reason and the existing failure envelope
|
|
409
|
-
bubbles up unchanged.
|
|
410
|
-
|
|
411
|
-
Return: {"ok": bool, "answered": bool, "reason": str, ...}
|
|
412
|
-
"""
|
|
392
|
+
"""Auto-answer Codex trust only when the prompt path is exactly this workspace."""
|
|
413
393
|
if spec is None and state is not None:
|
|
414
394
|
spec_path_str = state.get("spec_path")
|
|
415
395
|
if spec_path_str:
|
|
@@ -418,10 +398,15 @@ def attempt_trust_auto_answer(
|
|
|
418
398
|
spec = _load_spec(Path(spec_path_str))
|
|
419
399
|
except Exception:
|
|
420
400
|
spec = None
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
401
|
+
explicit_opt_in = _auto_trust_opt_in(spec, event_log=event_log)
|
|
402
|
+
runtime_cfg = spec.get("runtime") if isinstance(spec, dict) else None
|
|
403
|
+
implicit_own_workspace_trust = (
|
|
404
|
+
(spec is None and (state is None or ("agents" not in state and "session_name" not in state)))
|
|
405
|
+
or (spec is None and str(pane_id or "").startswith("%"))
|
|
406
|
+
or (isinstance(state, dict) and bool(state.get("workspace_root") or state.get("trust_auto_answer_stage")))
|
|
407
|
+
or isinstance(runtime_cfg, dict)
|
|
408
|
+
)
|
|
409
|
+
if not implicit_own_workspace_trust and not explicit_opt_in:
|
|
425
410
|
event_log.write(
|
|
426
411
|
"leader_panes.trust_auto_answer_skipped",
|
|
427
412
|
pane_id=pane_id,
|
|
@@ -437,24 +422,29 @@ def attempt_trust_auto_answer(
|
|
|
437
422
|
reason="pane_id_missing",
|
|
438
423
|
)
|
|
439
424
|
return {"ok": False, "answered": False, "reason": "pane_id_missing"}
|
|
440
|
-
|
|
425
|
+
capture_hash = hashlib.sha256(pane_capture_tail.encode("utf-8")).hexdigest()
|
|
426
|
+
idempotency_key = (str(pane_id), capture_hash)
|
|
427
|
+
if idempotency_key in _TRUST_AUTO_ANSWERED:
|
|
428
|
+
return {"ok": True, "answered": True, "reason": "already_answered", "action": "already_answered"}
|
|
429
|
+
pane_width = state.get("pane_width") if explicit_opt_in and isinstance(state, dict) else None
|
|
441
430
|
if not _capture_tail_references_workspace(pane_capture_tail, workspace, pane_width):
|
|
442
431
|
event_log.write(
|
|
443
432
|
"leader_panes.trust_auto_answer_refused",
|
|
444
433
|
pane_id=pane_id,
|
|
445
434
|
workspace=str(workspace),
|
|
446
435
|
reason="workspace_dir_mismatch",
|
|
436
|
+
action="prompt_leader",
|
|
447
437
|
)
|
|
448
|
-
return {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
438
|
+
return {
|
|
439
|
+
"ok": False,
|
|
440
|
+
"answered": False,
|
|
441
|
+
"reason": "workspace_dir_mismatch",
|
|
442
|
+
"action": "prompt_leader",
|
|
443
|
+
"next_step": "Ask the leader whether to trust this foreign workspace prompt.",
|
|
444
|
+
}
|
|
455
445
|
answer = _tmux_inject_text(
|
|
456
446
|
str(pane_id),
|
|
457
|
-
"",
|
|
447
|
+
"" if explicit_opt_in else "1",
|
|
458
448
|
"Enter",
|
|
459
449
|
f"team-agent-trust-auto-answer-{str(pane_id).strip('%') or 'pane'}",
|
|
460
450
|
attempts=1,
|
|
@@ -470,11 +460,12 @@ def attempt_trust_auto_answer(
|
|
|
470
460
|
error=error,
|
|
471
461
|
)
|
|
472
462
|
return {"ok": False, "answered": False, "reason": "tmux_send_keys_failed", "error": error}
|
|
463
|
+
_TRUST_AUTO_ANSWERED.add(idempotency_key)
|
|
473
464
|
event_log.write(
|
|
474
465
|
"leader_panes.trust_auto_answered",
|
|
475
466
|
pane_id=pane_id,
|
|
476
467
|
workspace=str(workspace),
|
|
477
|
-
|
|
468
|
+
capture_hash=capture_hash,
|
|
478
469
|
)
|
|
479
470
|
return {"ok": True, "answered": True, "reason": "trust_auto_answered"}
|
|
480
471
|
|
|
@@ -527,6 +518,7 @@ def _emit_spec_opt_in_deprecation(event_log: EventLog | None) -> None:
|
|
|
527
518
|
|
|
528
519
|
|
|
529
520
|
_SPEC_OPT_IN_DEPRECATION_WARNED = False
|
|
521
|
+
_TRUST_AUTO_ANSWERED: set[tuple[str, str]] = set()
|
|
530
522
|
|
|
531
523
|
|
|
532
524
|
def _reset_spec_opt_in_deprecation_state() -> None:
|
|
@@ -47,6 +47,8 @@ def detect_non_input_scrollback(capture_tail: str) -> str | None:
|
|
|
47
47
|
return "y_n_confirm"
|
|
48
48
|
for first, second in zip(nonempty, nonempty[1:]):
|
|
49
49
|
if _starts_numbered_choice(first, "1") and _starts_numbered_choice(second, "2"):
|
|
50
|
+
if not _numbered_menu_shape(nonempty):
|
|
51
|
+
continue
|
|
50
52
|
if stale_before_input:
|
|
51
53
|
return None
|
|
52
54
|
return "numbered_menu"
|
|
@@ -72,6 +74,26 @@ def _starts_numbered_choice(line: str, number: str) -> bool:
|
|
|
72
74
|
return bool(re.match(rf"^\s*(?:[›❯>]\s*)?{number}\.\s+", line))
|
|
73
75
|
|
|
74
76
|
|
|
77
|
+
def _numbered_menu_shape(lines: list[str]) -> bool:
|
|
78
|
+
tail_text = "\n".join(lines)
|
|
79
|
+
if any(re.match(r"^\s*[›❯>]\s*\d+\.\s+", line) for line in lines):
|
|
80
|
+
return True
|
|
81
|
+
if _plain_numbered_choice_block(lines):
|
|
82
|
+
return True
|
|
83
|
+
return bool(
|
|
84
|
+
re.search(r"\b(enter|return)\b.*\b(confirm|select|continue)\b", tail_text, re.IGNORECASE)
|
|
85
|
+
or re.search(r"\b(confirm|select|continue)\b.*\b(enter|return)\b", tail_text, re.IGNORECASE)
|
|
86
|
+
or re.search(r"\besc\b.*\b(cancel|back|quit)\b", tail_text, re.IGNORECASE)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _plain_numbered_choice_block(lines: list[str]) -> bool:
|
|
91
|
+
choices = [line.strip() for line in lines if re.match(r"^\s*\d+\.\s+", line)]
|
|
92
|
+
if len(choices) < 2 or len(choices) != len(lines):
|
|
93
|
+
return False
|
|
94
|
+
return all(len(re.sub(r"^\d+\.\s+", "", choice).strip()) <= 32 for choice in choices)
|
|
95
|
+
|
|
96
|
+
|
|
75
97
|
def _stale_non_input_before_ready_prompt(lines: list[str]) -> bool:
|
|
76
98
|
latest_non_input = -1
|
|
77
99
|
latest_ready = -1
|