@team-agent/installer 0.2.6 → 0.2.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "npx installer for Team Agent",
5
5
  "keywords": [
6
6
  "codex",
@@ -10,8 +10,8 @@ is sourced from the caller-supplied positive facts only:
10
10
 
11
11
  Reverse enumeration of panes / windows / clients is forbidden. Heuristic
12
12
  ranking ("active pane", "current client", "first leader-shaped pane") is
13
- forbidden. ``$TMUX_PANE`` missing or the caller pane not running a leader
14
- host refuse and emit ``owner.bind_refused``. Successful binds emit
13
+ forbidden. ``$TMUX_PANE`` missing refuse and emit ``owner.bind_refused``.
14
+ The pane's current command is diagnostic metadata only. Successful binds emit
15
15
  ``owner.bound_from_caller_pane`` and force-write every owner identity
16
16
  field; old fields are not merged or migrated.
17
17
  """
@@ -37,11 +37,9 @@ def run_cmd(args: list[str], timeout: int = 5) -> subprocess.CompletedProcess[st
37
37
  )
38
38
 
39
39
 
40
- LEADER_HOST_COMMANDS = frozenset({"claude", "claude.exe", "codex"})
41
-
42
40
  _HINT_RUN_FROM_LEADER_PANE = (
43
41
  "run team-agent from inside your leader pane "
44
- "(the tmux pane currently running claude or codex)."
42
+ "(the tmux pane you want to own this team)."
45
43
  )
46
44
 
47
45
 
@@ -71,7 +69,7 @@ def bind_owner_from_caller_pane(
71
69
 
72
70
  {
73
71
  "ok": False,
74
- "reason": "caller_pane_missing" | "caller_not_leader_shaped",
72
+ "reason": "caller_pane_missing",
75
73
  "caller_pane_id": ..., "caller_current_command": ...,
76
74
  "hint": ...,
77
75
  }
@@ -110,26 +108,6 @@ def bind_owner_from_caller_pane(
110
108
  caller_command = ""
111
109
  else:
112
110
  caller_command = (getattr(proc, "stdout", "") or "").strip()
113
- if caller_command not in LEADER_HOST_COMMANDS:
114
- hint = (
115
- f"run team-agent from inside your leader pane "
116
- f"(this pane is running {caller_command or '<unknown>'})."
117
- )
118
- event_log.write(
119
- "owner.bind_refused",
120
- reason="caller_not_leader_shaped",
121
- caller_pane_id=caller_pane,
122
- caller_current_command=caller_command,
123
- team_id=team_id,
124
- hint=hint,
125
- )
126
- return {
127
- "ok": False,
128
- "reason": "caller_not_leader_shaped",
129
- "caller_pane_id": caller_pane,
130
- "caller_current_command": caller_command,
131
- "hint": hint,
132
- }
133
111
  machine_fingerprint = os.environ.get("TEAM_AGENT_MACHINE_FINGERPRINT") or ""
134
112
  os_user = os.environ.get("USER") or os.environ.get("USERNAME") or ""
135
113
  provider = (
@@ -367,6 +367,18 @@ def restart(workspace: Path, allow_fresh: bool = False, team: str | None = None)
367
367
  from team_agent.leader import autobind_leader_receiver_from_env
368
368
  leader_provider = str(spec.get("leader", {}).get("provider") or "codex")
369
369
  rebound_receiver = autobind_leader_receiver_from_env(workspace, leader_provider, source="restart")
370
+ if rebound_receiver is None and state.get("leader_receiver"):
371
+ stale = state.pop("leader_receiver", None)
372
+ event_log.write(
373
+ "leader_receiver.rebind_required",
374
+ reason="restart_autobind_unresolved",
375
+ old_pane_id=(stale or {}).get("pane_id") if isinstance(stale, dict) else None,
376
+ old_session_name=(stale or {}).get("session_name") if isinstance(stale, dict) else None,
377
+ source="restart",
378
+ )
379
+ save_runtime_state(workspace, state)
380
+ save_team_runtime_snapshot(workspace, state)
381
+ write_team_state(workspace, spec, state)
370
382
  rebuild_restart_display_after_rebind(display_backend, workspace, session_name, spec, event_log, restarted, receiver=rebound_receiver)
371
383
  coordinator = start_coordinator(workspace)
372
384
  event_log.write("restart.complete", session=session_name, agents=restarted, coordinator=coordinator)
@@ -4,6 +4,7 @@ import hashlib
4
4
  import json
5
5
  import os
6
6
  import copy
7
+ import subprocess
7
8
  import uuid
8
9
  from datetime import datetime, timezone
9
10
  from pathlib import Path
@@ -313,7 +314,7 @@ def _caller_identity_from_env(state: dict[str, Any] | None = None, team_id: str
313
314
  team_id or os.environ.get("TEAM_AGENT_TEAM_ID") or team_state_key(state),
314
315
  )
315
316
  return {
316
- "pane_id": os.environ.get("TEAM_AGENT_LEADER_PANE_ID") or "",
317
+ "pane_id": os.environ.get("TEAM_AGENT_LEADER_PANE_ID") or os.environ.get("TMUX_PANE") or "",
317
318
  "provider": os.environ.get("TEAM_AGENT_LEADER_PROVIDER") or "",
318
319
  "machine_fingerprint": machine_fingerprint,
319
320
  "leader_session_uuid": leader_uuid,
@@ -321,6 +322,36 @@ def _caller_identity_from_env(state: dict[str, Any] | None = None, team_id: str
321
322
  }
322
323
 
323
324
 
325
+ _TMUX_PANE_LIVE = "live"
326
+ _TMUX_PANE_DEAD = "dead"
327
+ _TMUX_PANE_UNKNOWN = "unknown"
328
+
329
+
330
+ def _tmux_pane_liveness(pane_id: str) -> str:
331
+ if not pane_id:
332
+ return _TMUX_PANE_UNKNOWN
333
+ try:
334
+ from team_agent.runtime import run_cmd
335
+ proc = run_cmd(["tmux", "display-message", "-p", "-t", pane_id, "#{pane_id}"], timeout=3)
336
+ except Exception:
337
+ try:
338
+ proc = subprocess.run(
339
+ ["tmux", "display-message", "-p", "-t", pane_id, "#{pane_id}"],
340
+ text=True,
341
+ capture_output=True,
342
+ timeout=3,
343
+ check=False,
344
+ )
345
+ except Exception:
346
+ return _TMUX_PANE_UNKNOWN
347
+ if proc.returncode == 0:
348
+ return _TMUX_PANE_LIVE
349
+ stderr = str(getattr(proc, "stderr", "") or "").lower()
350
+ if "can't find pane" in stderr or "can't find window" in stderr or "can't find session" in stderr:
351
+ return _TMUX_PANE_DEAD
352
+ return _TMUX_PANE_UNKNOWN
353
+
354
+
324
355
  def check_team_owner(state: dict[str, Any]) -> dict[str, Any] | None:
325
356
  owner = state.get("team_owner") or {}
326
357
  if not owner:
@@ -331,6 +362,15 @@ def check_team_owner(state: dict[str, Any]) -> dict[str, Any] | None:
331
362
  caller_uuid = caller["leader_session_uuid"]
332
363
  owner_pane = str(owner.get("pane_id") or "")
333
364
  caller_pane = caller.get("pane_id") or ""
365
+ if caller_pane and caller_pane == owner_pane:
366
+ return None
367
+ if (
368
+ caller_pane
369
+ and not os.environ.get("TEAM_AGENT_ID")
370
+ and owner_pane
371
+ and _tmux_pane_liveness(owner_pane) != _TMUX_PANE_LIVE
372
+ ):
373
+ return None
334
374
  if caller_uuid == owner_uuid and (not caller_pane or caller_pane == owner_pane):
335
375
  return None
336
376
  same_uuid = caller_uuid == owner_uuid