@team-agent/installer 0.1.9 → 0.1.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/README.md CHANGED
@@ -128,7 +128,12 @@ Lead: [proposes a team — refactor architect (Claude), code mover (Codex),
128
128
  You: Go.
129
129
  ```
130
130
 
131
- That's it. Teammates appear in separate windows. The lead reports progress, raises decisions when needed, and shuts everything down when you say so.
131
+ That's it. With the default display, teammates appear in separate Ghostty
132
+ windows. If `display_backend: ghostty_workspace` is set, teammates appear in one
133
+ Ghostty window with tmux tabs/windows, up to three side-by-side panes per tab
134
+ (`4` workers => `3 + 1`, `8` workers => `3 + 3 + 2`). The lead reports
135
+ progress, raises decisions when needed, and shuts everything down when you say
136
+ so.
132
137
 
133
138
  ### Stop / resume
134
139
 
package/README.zh.md CHANGED
@@ -124,7 +124,10 @@ Lead: [提议一个 team:重构架构师 (Claude)、代码搬运工 (Codex)、
124
124
  你: 开始。
125
125
  ```
126
126
 
127
- 完事。队员窗口出现,lead 推进工作、需要决策时停下来问你,你说"关掉"就关掉。
127
+ 完事。默认显示模式下,队员会分别出现在独立 Ghostty 窗口。设置
128
+ `display_backend: ghostty_workspace` 时,队员会出现在同一个 Ghostty 窗口里,
129
+ 用 tmux tab/window 分页,每个 tab 最多三列 pane:4 个队员是 `3 + 1`,8 个队员是
130
+ `3 + 3 + 2`。lead 推进工作、需要决策时停下来问你,你说"关掉"就关掉。
128
131
 
129
132
  ### 关闭 / 恢复
130
133
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "npx installer for Team Agent",
5
5
  "keywords": [
6
6
  "codex",
@@ -27,7 +27,7 @@ cat > .team/current/TEAM.md <<'EOF'
27
27
  name: demo-team
28
28
  objective: One worker handles bounded tasks and reports through Team Agent MCP.
29
29
  dangerous_auto_approve: false
30
- display_backend: ghostty_window
30
+ display_backend: ghostty_workspace
31
31
  fast: false
32
32
  provider_models:
33
33
  codex: gpt-5.5
@@ -59,7 +59,13 @@ team-agent quick-start .team/current
59
59
  ```
60
60
 
61
61
  YAML lists must be block style. Use `tools:\n - fs_read`; do not use `tools: [fs_read, mcp_team]`.
62
- Omitting `display_backend` defaults to `ghostty_window`; set `display_backend: none` only for headless/CI runs.
62
+
63
+ Display choices:
64
+ - `ghostty_workspace`: one Ghostty window. Workers are shown in tmux tabs/windows, up to 3 side-by-side panes per tab. Four workers become `3 + 1`; eight become `3 + 3 + 2`.
65
+ - `ghostty_window`: one Ghostty window per worker.
66
+ - `none`: headless/CI.
67
+
68
+ Omitting `display_backend` defaults to `ghostty_window`.
63
69
 
64
70
  ## Provider Prep
65
71
 
@@ -55,6 +55,7 @@ TMUX_PANE_FORMAT = (
55
55
  )
56
56
  HEALTH_STATUSES = {"RUNNING", "IDLE", "AWAITING_APPROVAL", "BLOCKED", "ERROR", "DONE"}
57
57
  GHOSTTY_DISPLAY_BACKENDS = {"ghostty", "ghostty_window", "ghostty_workspace"}
58
+ GHOSTTY_WORKSPACE_PANES_PER_WINDOW = 3
58
59
  PEEK_MAX_LINES = 80
59
60
  PEEK_SEARCH_SCAN_LINES = 300
60
61
  PEEK_MAX_MATCHES = 5
@@ -227,6 +228,22 @@ def _spec_team_dir(spec_path: Path, workspace: Path) -> Path:
227
228
  return workspace.resolve() / ".team" / "current"
228
229
 
229
230
 
231
+ def _is_team_doc_dir(team_dir: Path) -> bool:
232
+ return (team_dir / "TEAM.md").exists() and (team_dir / "agents").is_dir()
233
+
234
+
235
+ def _compile_team_dir_spec(team_dir: Path, workspace: Path) -> dict[str, Any]:
236
+ from team_agent.compiler import compile_team
237
+
238
+ spec_path = team_dir / "team.spec.yaml"
239
+ compiled = compile_team(team_dir, spec_path)
240
+ if compiled["spec"].get("context", {}).get("state_file") == "team_state.md":
241
+ state_file = str(team_dir.relative_to(workspace) / "team_state.md") if team_dir.is_relative_to(workspace) else "team_state.md"
242
+ compiled["spec"]["context"]["state_file"] = state_file
243
+ spec_path.write_text(dumps(compiled["spec"]), encoding="utf-8")
244
+ return compiled
245
+
246
+
230
247
  def _attach_team_profile_dirs(spec: dict[str, Any], spec_path: Path, workspace: Path | None = None, team_dir: Path | None = None) -> None:
231
248
  workspace = workspace.resolve() if workspace else workspace_from_spec(spec, spec_path)
232
249
  team_dir = team_dir.resolve() if team_dir else _spec_team_dir(spec_path, workspace)
@@ -1766,8 +1783,12 @@ def shutdown(workspace: Path, keep_logs: bool = True) -> dict[str, Any]:
1766
1783
  closed_displays.add(agent_id)
1767
1784
  proc = run_cmd(["tmux", "kill-session", "-t", session_name], timeout=10)
1768
1785
  if proc.returncode != 0:
1769
- raise RuntimeError(f"tmux kill-session failed: {proc.stderr.strip()}")
1770
- event_log.write("shutdown.kill_session", session=session_name, keep_logs=keep_logs, captured=captured)
1786
+ if "can't find session" in proc.stderr:
1787
+ event_log.write("shutdown.idempotent", session=session_name, reason="session disappeared before kill")
1788
+ else:
1789
+ raise RuntimeError(f"tmux kill-session failed: {proc.stderr.strip()}")
1790
+ else:
1791
+ event_log.write("shutdown.kill_session", session=session_name, keep_logs=keep_logs, captured=captured)
1771
1792
  else:
1772
1793
  event_log.write("shutdown.idempotent", session=session_name, reason="session missing")
1773
1794
  _close_ghostty_workspace(state, event_log)
@@ -1801,10 +1822,16 @@ def shutdown(workspace: Path, keep_logs: bool = True) -> dict[str, Any]:
1801
1822
  def restart(workspace: Path, allow_fresh: bool = False, team: str | None = None) -> dict[str, Any]:
1802
1823
  state = _select_restart_state(workspace, team)
1803
1824
  spec_path = Path(state.get("spec_path", workspace / "team.spec.yaml"))
1804
- if not spec_path.exists():
1805
- raise RuntimeError(f"missing spec for restart: {spec_path}")
1806
- spec = load_spec(spec_path)
1807
1825
  team_dir = Path(str(state.get("team_dir"))) if state.get("team_dir") else _spec_team_dir(spec_path, workspace)
1826
+ if _is_team_doc_dir(team_dir):
1827
+ compiled = _compile_team_dir_spec(team_dir, workspace)
1828
+ spec = compiled["spec"]
1829
+ spec_path = team_dir / "team.spec.yaml"
1830
+ state["spec_path"] = str(spec_path)
1831
+ else:
1832
+ if not spec_path.exists():
1833
+ raise RuntimeError(f"missing spec for restart: {spec_path}")
1834
+ spec = load_spec(spec_path)
1808
1835
  _attach_team_profile_dirs(spec, spec_path, workspace, team_dir)
1809
1836
  ensure_workspace_dirs(workspace)
1810
1837
  event_log = EventLog(workspace)
@@ -1819,6 +1846,9 @@ def restart(workspace: Path, allow_fresh: bool = False, team: str | None = None)
1819
1846
  raise RuntimeError(_tmux_session_conflict_error(session_name))
1820
1847
  runtime_cfg = _effective_runtime_config(spec.get("runtime", {}))
1821
1848
  display_backend = spec.get("runtime", {}).get("display_backend", state.get("display_backend", "none"))
1849
+ _close_ghostty_workspace(state, event_log)
1850
+ for agent_id, agent_state in state.get("agents", {}).items():
1851
+ _close_ghostty_display(agent_id, agent_state, event_log)
1822
1852
  state["display_backend"] = display_backend
1823
1853
  restart_agents = [
1824
1854
  agent
@@ -3126,18 +3156,12 @@ def quick_start(
3126
3156
  fresh: bool = False,
3127
3157
  team_id: str | None = None,
3128
3158
  ) -> dict[str, Any]:
3129
- from team_agent.compiler import compile_team
3130
-
3131
3159
  team_dir = _prepare_quick_start_team(agents_dir.resolve(), Path.cwd().resolve(), name, team_id=team_id)
3132
3160
  workspace = team_workspace(team_dir)
3133
3161
  ensure_workspace_dirs(workspace)
3134
3162
  _ensure_profiles_for_roles(team_dir)
3163
+ compiled = _compile_team_dir_spec(team_dir, workspace)
3135
3164
  spec_path = team_dir / "team.spec.yaml"
3136
- compiled = compile_team(team_dir, spec_path)
3137
- if compiled["spec"].get("context", {}).get("state_file") == "team_state.md":
3138
- state_file = str(team_dir.relative_to(workspace) / "team_state.md") if team_dir.is_relative_to(workspace) else "team_state.md"
3139
- compiled["spec"]["context"]["state_file"] = state_file
3140
- spec_path.write_text(dumps(compiled["spec"]), encoding="utf-8")
3141
3165
  existing = _quick_start_existing_context(workspace, compiled["spec"]["runtime"]["session_name"])
3142
3166
  if existing and not fresh:
3143
3167
  return {
@@ -4715,6 +4739,7 @@ def _open_ghostty_workspace(
4715
4739
  "linked_session": linked_session,
4716
4740
  "aggregator_session": aggregator_session,
4717
4741
  "display_session": aggregator_session,
4742
+ "workspace_window": pane.get("window_name"),
4718
4743
  "pane_id": pane.get("pane_id"),
4719
4744
  "launch_args": launch_args,
4720
4745
  "pid": pids[0] if pids else None,
@@ -4790,6 +4815,10 @@ def _ghostty_workspace_aggregator_name(session_name: str) -> str:
4790
4815
  return f"{safe_session}__display__workspace__{digest}"
4791
4816
 
4792
4817
 
4818
+ def _ghostty_workspace_window_name(index: int) -> str:
4819
+ return "overview" if index == 0 else f"overview-{index + 1}"
4820
+
4821
+
4793
4822
  def _ghostty_workspace_pane_command(linked_session: str) -> str:
4794
4823
  return f"TMUX= tmux attach-session -t {shlex.quote(linked_session)}"
4795
4824
 
@@ -4833,75 +4862,121 @@ def _prepare_ghostty_workspace_aggregator(
4833
4862
  aggregator_session: str,
4834
4863
  linked_jobs: list[tuple[str, dict[str, Any], str]],
4835
4864
  ) -> dict[str, Any]:
4836
- window_name = "overview"
4837
4865
  if _tmux_session_exists(aggregator_session):
4838
4866
  proc = run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
4839
4867
  if proc.returncode != 0:
4840
4868
  return {"ok": False, "reason": "display_session_cleanup_failed", "error": proc.stderr.strip()}
4841
4869
 
4842
- panes: list[dict[str, Any]] = []
4843
- first_agent_id, first_agent, first_linked_session = linked_jobs[0]
4844
- proc = run_cmd(
4845
- [
4846
- "tmux",
4847
- "new-session",
4848
- "-d",
4849
- "-P",
4850
- "-F",
4851
- "#{pane_id}",
4852
- "-s",
4853
- aggregator_session,
4854
- "-n",
4855
- window_name,
4856
- _ghostty_workspace_pane_command(first_linked_session),
4857
- ],
4858
- timeout=10,
4859
- )
4860
- if proc.returncode != 0:
4861
- return {"ok": False, "reason": "display_session_create_failed", "error": proc.stderr.strip()}
4862
- first_pane_id = _tmux_stdout_last_line(proc.stdout) or f"{aggregator_session}:{window_name}.0"
4863
- first_title = _ghostty_workspace_pane_title(first_agent)
4864
- title_result = _set_ghostty_workspace_pane_title(first_pane_id, first_title)
4865
- if not title_result["ok"]:
4870
+ def fail(reason: str, proc: Any | None = None, target: str | None = None) -> dict[str, Any]:
4866
4871
  run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
4867
- return title_result
4868
- panes.append({"agent_id": first_agent_id, "pane_id": first_pane_id, "title": first_title, "linked_session": first_linked_session})
4869
-
4870
- proc = run_cmd(["tmux", "set-window-option", "-t", f"{aggregator_session}:{window_name}", "remain-on-exit", "on"], timeout=10)
4871
- if proc.returncode != 0:
4872
- run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
4873
- return {"ok": False, "reason": "display_session_remain_on_exit_failed", "error": proc.stderr.strip()}
4872
+ result = {"ok": False, "reason": reason}
4873
+ if proc is not None:
4874
+ result["error"] = proc.stderr.strip()
4875
+ if target:
4876
+ result["target"] = target
4877
+ return result
4874
4878
 
4875
- for index, (agent_id, agent, linked_session) in enumerate(linked_jobs[1:], start=1):
4876
- proc = run_cmd(
4877
- [
4878
- "tmux",
4879
- "split-window",
4880
- "-t",
4881
- f"{aggregator_session}:{window_name}",
4882
- "-h",
4883
- "-P",
4884
- "-F",
4885
- "#{pane_id}",
4886
- _ghostty_workspace_pane_command(linked_session),
4887
- ],
4888
- timeout=10,
4879
+ panes: list[dict[str, Any]] = []
4880
+ for window_index, start in enumerate(range(0, len(linked_jobs), GHOSTTY_WORKSPACE_PANES_PER_WINDOW)):
4881
+ window_name = _ghostty_workspace_window_name(window_index)
4882
+ window_jobs = linked_jobs[start : start + GHOSTTY_WORKSPACE_PANES_PER_WINDOW]
4883
+ first_agent_id, first_agent, first_linked_session = window_jobs[0]
4884
+ if window_index == 0:
4885
+ proc = run_cmd(
4886
+ [
4887
+ "tmux",
4888
+ "new-session",
4889
+ "-d",
4890
+ "-P",
4891
+ "-F",
4892
+ "#{pane_id}",
4893
+ "-s",
4894
+ aggregator_session,
4895
+ "-n",
4896
+ window_name,
4897
+ _ghostty_workspace_pane_command(first_linked_session),
4898
+ ],
4899
+ timeout=10,
4900
+ )
4901
+ if proc.returncode != 0:
4902
+ return {"ok": False, "reason": "display_session_create_failed", "error": proc.stderr.strip()}
4903
+ else:
4904
+ proc = run_cmd(
4905
+ [
4906
+ "tmux",
4907
+ "new-window",
4908
+ "-t",
4909
+ aggregator_session,
4910
+ "-n",
4911
+ window_name,
4912
+ "-P",
4913
+ "-F",
4914
+ "#{pane_id}",
4915
+ _ghostty_workspace_pane_command(first_linked_session),
4916
+ ],
4917
+ timeout=10,
4918
+ )
4919
+ if proc.returncode != 0:
4920
+ return fail("display_session_window_create_failed", proc, first_linked_session)
4921
+ first_pane_id = _tmux_stdout_last_line(proc.stdout) or f"{aggregator_session}:{window_name}.0"
4922
+ first_title = _ghostty_workspace_pane_title(first_agent)
4923
+ title_result = _set_ghostty_workspace_pane_title(first_pane_id, first_title)
4924
+ if not title_result["ok"]:
4925
+ return fail(title_result["reason"], target=first_pane_id)
4926
+ panes.append(
4927
+ {
4928
+ "agent_id": first_agent_id,
4929
+ "pane_id": first_pane_id,
4930
+ "title": first_title,
4931
+ "linked_session": first_linked_session,
4932
+ "window_name": window_name,
4933
+ }
4889
4934
  )
4935
+
4936
+ proc = run_cmd(["tmux", "set-window-option", "-t", f"{aggregator_session}:{window_name}", "remain-on-exit", "on"], timeout=10)
4890
4937
  if proc.returncode != 0:
4891
- run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
4892
- return {"ok": False, "reason": "display_session_split_failed", "error": proc.stderr.strip(), "target": linked_session}
4893
- pane_id = _tmux_stdout_last_line(proc.stdout) or f"{aggregator_session}:{window_name}.{index}"
4894
- title = _ghostty_workspace_pane_title(agent)
4895
- title_result = _set_ghostty_workspace_pane_title(pane_id, title)
4896
- if not title_result["ok"]:
4897
- run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
4898
- return title_result
4899
- panes.append({"agent_id": agent_id, "pane_id": pane_id, "title": title, "linked_session": linked_session})
4938
+ return fail("display_session_remain_on_exit_failed", proc)
4939
+
4940
+ for index, (agent_id, agent, linked_session) in enumerate(window_jobs[1:], start=1):
4941
+ proc = run_cmd(
4942
+ [
4943
+ "tmux",
4944
+ "split-window",
4945
+ "-t",
4946
+ f"{aggregator_session}:{window_name}",
4947
+ "-h",
4948
+ "-P",
4949
+ "-F",
4950
+ "#{pane_id}",
4951
+ _ghostty_workspace_pane_command(linked_session),
4952
+ ],
4953
+ timeout=10,
4954
+ )
4955
+ if proc.returncode != 0:
4956
+ return fail("display_session_split_failed", proc, linked_session)
4957
+ pane_id = _tmux_stdout_last_line(proc.stdout) or f"{aggregator_session}:{window_name}.{index}"
4958
+ title = _ghostty_workspace_pane_title(agent)
4959
+ title_result = _set_ghostty_workspace_pane_title(pane_id, title)
4960
+ if not title_result["ok"]:
4961
+ return fail(title_result["reason"], target=pane_id)
4962
+ panes.append(
4963
+ {
4964
+ "agent_id": agent_id,
4965
+ "pane_id": pane_id,
4966
+ "title": title,
4967
+ "linked_session": linked_session,
4968
+ "window_name": window_name,
4969
+ }
4970
+ )
4971
+
4972
+ proc = run_cmd(["tmux", "select-layout", "-t", f"{aggregator_session}:{window_name}", "even-horizontal"], timeout=10)
4973
+ if proc.returncode != 0:
4974
+ return fail("display_session_layout_failed", proc)
4900
4975
 
4901
- proc = run_cmd(["tmux", "select-layout", "-t", f"{aggregator_session}:{window_name}", "even-horizontal"], timeout=10)
4976
+ proc = run_cmd(["tmux", "set-option", "-t", aggregator_session, "mouse", "on"], timeout=10)
4902
4977
  if proc.returncode != 0:
4903
- run_cmd(["tmux", "kill-session", "-t", aggregator_session], timeout=10)
4904
- return {"ok": False, "reason": "display_session_layout_failed", "error": proc.stderr.strip()}
4978
+ return fail("display_session_mouse_failed", proc)
4979
+ run_cmd(["tmux", "select-window", "-t", f"{aggregator_session}:{_ghostty_workspace_window_name(0)}"], timeout=10)
4905
4980
  return {"ok": True, "aggregator_session": aggregator_session, "panes": panes}
4906
4981
 
4907
4982
 
@@ -4963,6 +5038,7 @@ def _open_ghostty_workspace_agent_display(
4963
5038
  pane_title = _ghostty_workspace_pane_title(agent)
4964
5039
  command = _ghostty_workspace_pane_command(linked_session)
4965
5040
  pane_id = str(previous_display.get("pane_id") or "")
5041
+ workspace_window = str(previous_display.get("workspace_window") or _ghostty_workspace_window_name(0))
4966
5042
  refreshed = False
4967
5043
  if pane_id:
4968
5044
  proc = run_cmd(["tmux", "respawn-pane", "-k", "-t", pane_id, command], timeout=10)
@@ -4973,7 +5049,7 @@ def _open_ghostty_workspace_agent_display(
4973
5049
  "tmux",
4974
5050
  "split-window",
4975
5051
  "-t",
4976
- f"{aggregator_session}:overview",
5052
+ f"{aggregator_session}:{workspace_window}",
4977
5053
  "-h",
4978
5054
  "-P",
4979
5055
  "-F",
@@ -5002,7 +5078,7 @@ def _open_ghostty_workspace_agent_display(
5002
5078
  reason=title_result["reason"],
5003
5079
  note=title_result.get("error") or "pane refresh requires full team restart",
5004
5080
  )
5005
- run_cmd(["tmux", "select-layout", "-t", f"{aggregator_session}:overview", "even-horizontal"], timeout=10)
5081
+ run_cmd(["tmux", "select-layout", "-t", f"{aggregator_session}:{workspace_window}", "even-horizontal"], timeout=10)
5006
5082
  title = str(previous_display.get("title") or f"team-agent:{session_name}:workspace")
5007
5083
  pids = [int(pid) for pid in previous_display.get("pids", []) if str(pid).isdigit()]
5008
5084
  display = {
@@ -5014,6 +5090,7 @@ def _open_ghostty_workspace_agent_display(
5014
5090
  "linked_session": linked_session,
5015
5091
  "aggregator_session": aggregator_session,
5016
5092
  "display_session": aggregator_session,
5093
+ "workspace_window": workspace_window,
5017
5094
  "pane_id": pane_id,
5018
5095
  "pid": pids[0] if pids else None,
5019
5096
  "pids": pids,