@team-agent/installer 0.5.98 → 0.5.99

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 (60) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +0 -7
  4. package/crates/team-agent/src/cli/diagnose.rs +0 -16
  5. package/crates/team-agent/src/cli/emit.rs +15 -20
  6. package/crates/team-agent/src/cli/mod.rs +130 -31
  7. package/crates/team-agent/src/cli/spec.rs +7 -7
  8. package/crates/team-agent/src/cli/tests/main_preserved.rs +0 -1
  9. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +0 -1
  10. package/crates/team-agent/src/cli/tests/run_delegation.rs +0 -3
  11. package/crates/team-agent/src/cli/tests/verb_validate.rs +0 -1
  12. package/crates/team-agent/src/cli/types.rs +0 -6
  13. package/crates/team-agent/src/compiler/tests.rs +5 -8
  14. package/crates/team-agent/src/compiler.rs +6 -14
  15. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +1 -1
  16. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +1 -1
  17. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +5 -25
  18. package/crates/team-agent/src/lifecycle/launch/identity.rs +2 -18
  19. package/crates/team-agent/src/lifecycle/launch/layout.rs +7 -29
  20. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +1 -1
  21. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +1 -1
  22. package/crates/team-agent/src/lifecycle/launch/ownership.rs +1 -1
  23. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +7 -7
  24. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +12 -49
  25. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +3 -9
  26. package/crates/team-agent/src/lifecycle/launch/readiness.rs +1 -1
  27. package/crates/team-agent/src/lifecycle/launch/spawn.rs +2 -61
  28. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +2 -15
  29. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +3 -7
  30. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +1 -1
  31. package/crates/team-agent/src/lifecycle/launch.rs +3 -4
  32. package/crates/team-agent/src/lifecycle/mod.rs +0 -8
  33. package/crates/team-agent/src/lifecycle/restart/agent.rs +62 -8
  34. package/crates/team-agent/src/lifecycle/restart/common.rs +0 -32
  35. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +0 -9
  36. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +1 -2
  37. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +0 -1
  38. package/crates/team-agent/src/lifecycle/tests/core.rs +0 -213
  39. package/crates/team-agent/src/lifecycle/tests/issue238_codex_effort_contract.rs +96 -0
  40. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +1 -0
  41. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +31 -77
  42. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +2 -2
  43. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +4 -8
  44. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +7 -7
  45. package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +1 -1
  46. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +3 -4
  47. package/crates/team-agent/src/lifecycle/tests.rs +1 -2
  48. package/crates/team-agent/src/lifecycle/types.rs +3 -133
  49. package/crates/team-agent/src/model/enums.rs +21 -71
  50. package/crates/team-agent/src/model/spec.rs +5 -20
  51. package/crates/team-agent/src/provider/adapters/codex.rs +2 -2
  52. package/crates/team-agent/src/provider/types.rs +6 -5
  53. package/examples/team.spec.yaml +0 -1
  54. package/package.json +4 -4
  55. package/schemas/team.schema.json +2 -2
  56. package/skills/team-agent/SKILL.md +1 -1
  57. package/skills/team-agent/references/team-agent-operator.md +3 -10
  58. package/crates/team-agent/src/lifecycle/display.rs +0 -550
  59. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +0 -884
  60. package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +0 -481
@@ -352,11 +352,12 @@ pub struct ProviderCommandContext<'a> {
352
352
  /// from role doc / TEAM.md / provider default. `None` means the
353
353
  /// framework passes no effort flag (provider default).
354
354
  /// - Claude / ClaudeCode: low|medium|high|xhigh|max → `--effort <level>`
355
- /// - Codex: low|medium|high|xhigh → `-c model_reasoning_effort=<level>`
356
- /// - Grok: low|medium|high|xhigh (NOT max) → `--effort <level>`
357
- /// - Copilot / Gemini / CursorAgent / Fake: ignored, warning event
358
- /// emitted at the caller (lifecycle/launch.rs / lifecycle/restart)
359
- /// before construct.
355
+ /// - Codex: low|medium|high|xhigh|max|ultra → `-c model_reasoning_effort=<level>`
356
+ /// Ultra enables Codex client delegation on top of max reasoning.
357
+ /// - Pi: low|medium|high|xhigh|max → `--thinking <level>`; ultra rejected
358
+ /// - Grok: low|medium|high|xhigh → `--effort <level>`; max/ultra rejected
359
+ /// - Copilot / Gemini / Fake: low..xhigh ignored with a caller warning;
360
+ /// max/ultra rejected. CursorAgent rejects all effort.
360
361
  pub effort: Option<crate::model::enums::ProviderEffort>,
361
362
  }
362
363
 
@@ -136,7 +136,6 @@ communication:
136
136
  mirror_files: .team/messages
137
137
  runtime:
138
138
  backend: tmux
139
- display_backend: none
140
139
  session_name: teamspec-full-example
141
140
  auto_launch: true
142
141
  require_user_approval_before_launch: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.5.98",
3
+ "version": "0.5.99",
4
4
  "description": "npx installer for Team Agent",
5
5
  "keywords": [
6
6
  "codex",
@@ -20,9 +20,9 @@
20
20
  "team-agent-installer": "npm/install.mjs"
21
21
  },
22
22
  "optionalDependencies": {
23
- "@team-agent/cli-darwin-arm64": "0.5.98",
24
- "@team-agent/cli-darwin-x64": "0.5.98",
25
- "@team-agent/cli-linux-x64": "0.5.98"
23
+ "@team-agent/cli-darwin-arm64": "0.5.99",
24
+ "@team-agent/cli-darwin-x64": "0.5.99",
25
+ "@team-agent/cli-linux-x64": "0.5.99"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",
@@ -59,11 +59,11 @@
59
59
  },
60
60
  "runtime": {
61
61
  "type": "object",
62
- "required": ["backend", "display_backend", "session_name", "auto_launch", "require_user_approval_before_launch", "max_active_agents", "startup_order"],
62
+ "required": ["backend", "session_name", "auto_launch", "require_user_approval_before_launch", "max_active_agents", "startup_order"],
63
63
  "additionalProperties": false,
64
64
  "properties": {
65
65
  "backend": { "enum": ["tmux", "pty"] },
66
- "display_backend": { "enum": ["none", "tmux_attach", "iterm", "ghostty", "ghostty_window", "ghostty_workspace"] },
66
+ "display_backend": {},
67
67
  "session_name": { "type": "string", "minLength": 1 },
68
68
  "auto_launch": { "type": "boolean" },
69
69
  "require_user_approval_before_launch": { "type": "boolean" },
@@ -17,7 +17,7 @@ team-agent --help
17
17
  team-agent doctor --help
18
18
  ```
19
19
 
20
- **Launch** from a tmux-addressable pane: `team-agent claude` or `team-agent codex`, then `team-agent quick-start .team/current`. Do not start a real team from a naked terminal. Existing tmux/Ghostty layouts are valid if `quick-start` runs from the leader pane.
20
+ **Launch** from a tmux-addressable pane: `team-agent claude` or `team-agent codex`, then `team-agent quick-start .team/current`. Do not start a real team from a naked terminal. Workers always run in independent background windows on the workspace-scoped tmux server.
21
21
 
22
22
  **Operate**
23
23
 
@@ -43,7 +43,7 @@ team-agent codex
43
43
  team-agent claude
44
44
  ```
45
45
 
46
- Pass provider flags after the provider name, for example `team-agent codex --dangerously-bypass-approvals-and-sandbox`. Existing tmux layouts are valid too, including Finder/Ghostty launchers, as long as `team-agent quick-start` is invoked from the leader's current tmux pane. Do not start a real team from a naked terminal that Team Agent cannot address through tmux.
46
+ Pass provider flags after the provider name, for example `team-agent codex --dangerously-bypass-approvals-and-sandbox`. Run `team-agent quick-start` from the leader's current tmux pane. Do not start a real team from a naked terminal that Team Agent cannot address through tmux.
47
47
 
48
48
  **0.5.66:** leader `--dangerously-*` flags no longer inherit bypass to workers. Per-worker bypass is the role field `dangerously_skip_permissions` (see Permissions).
49
49
 
@@ -98,14 +98,7 @@ team-agent quick-start .team/current
98
98
 
99
99
  YAML lists must be block style. Use `tools:\n - fs_read`; do not use `tools: [fs_read, mcp_team]`.
100
100
 
101
- Display choices (set `display_backend:` in `TEAM.md` to opt in):
102
-
103
- - `none` (default): headless / no GUI window manager. The team runs entirely in the per-workspace tmux server; this is what the demo above uses.
104
- - `adaptive`: framework picks an available GUI layout for the local platform.
105
- - `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`.
106
- - `ghostty_window`: one Ghostty window per worker.
107
-
108
- **Omitting `display_backend` defaults to `none`** (changed in 0.3.4). Set `display_backend: adaptive` (or one of the explicit ghostty variants) in `TEAM.md` only when the user wants GUI windows.
101
+ Workers always run headlessly in independent windows on the per-workspace tmux server. No GUI display backend is configured in `TEAM.md`.
109
102
 
110
103
  ## Private Tmux Socket
111
104
 
@@ -267,7 +260,7 @@ Observed (all exit 0):
267
260
 
268
261
  ```text
269
262
  usage: team-agent restart [WORKSPACE] [--team TEAM] [--allow-fresh] [--session-converge-deadline SECONDS] [--json]
270
- usage: team-agent add-agent AGENT --role-file FILE [--force] [--workspace WORKSPACE] [--team TEAM] [--no-display] [--json]
263
+ usage: team-agent add-agent AGENT --role-file FILE [--force] [--workspace WORKSPACE] [--team TEAM] [--json]
271
264
  usage: team-agent shutdown [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]
272
265
  ```
273
266
 
@@ -1,550 +0,0 @@
1
- //! ---
2
- //! purpose: team 显示面的能力探测、后端解析、开关与 rebind 后重建
3
- //! contract:
4
- //! provides:
5
- //! - name: resolve_display_backend
6
- //! what: 按 requested 优先于 recorded 优先于 adaptive 选后端,并标出是否偏离默认
7
- //! - name: probe_display_capabilities
8
- //! what: 探测当前进程能否开 adaptive 显示,给出被挡原因
9
- //! - name: open_worker_displays
10
- //! what: 按后端为各 worker 打开显示,返回 agent 到 WorkerDisplay 的映射
11
- //! - name: close_team_display_backends
12
- //! what: 关闭本 team 的显示后端
13
- //! - name: rebuild_adaptive_display_after_rebind
14
- //! what: leader rebind 之后重建 adaptive 窗口
15
- //! depends:
16
- //! - crate::transport
17
- //! - crate::state::persist
18
- //! boundary:
19
- //! - 不 spawn 也不 kill worker 进程,只管显示面
20
- //! - 显示失败不阻塞 team readiness
21
- //! - tmux 操作一律走 workspace 作用域的 TmuxBackend,不用继承 $TMUX 的 ambient 调用
22
- //! - ambient leader-pane 探测不属于本文件
23
- //! maturity: wired
24
- //! ---
25
- //!
26
- //! lifecycle::display —— 能力门 / 后端解析 / 开关 / rebind 后重建。
27
- //!
28
- //! 0.5.39 Slice 1 (tmux-server-death-locate §7 Slice 1): all tmux
29
- //! session/window/pane operations here MUST go through the workspace's
30
- //! scoped `TmuxBackend` — raw `Command::new("tmux")` / ambient
31
- //! `run_tmux(...)` helpers in this file inherit ambient `$TMUX` and can
32
- //! kill sessions on the wrong socket. Ambient leader-pane env probes
33
- //! (`display-message` reading $TMUX to discover which session invoked
34
- //! us) live in `tmux_backend::probe_ambient_leader_pane_info` — that
35
- //! probe is definitionally ambient (its whole job is "which session am I
36
- //! in") and stays outside this module.
37
-
38
- use std::collections::BTreeMap;
39
- use std::path::Path;
40
-
41
- use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
42
-
43
- use super::*;
44
-
45
- // ── lifecycle::display —— 能力门 / 后端解析 / 开关 / rebind 后重建 ────────────
46
-
47
- /// ---
48
- /// purpose: 定出本次使用的显示后端
49
- /// params:
50
- /// requested: 本次命令显式请求的后端,None 表示未指定
51
- /// recorded: state 里记录的上次后端
52
- /// returns: 选中的后端,附带它是否偏离默认 adaptive
53
- /// ---
54
- ///
55
- /// `resolve_display_backend(requested, recorded, source)`(`display/backend.py`)。默认
56
- /// adaptive;显式 none 是逃生口。
57
- pub fn resolve_display_backend(
58
- requested: Option<DisplayBackend>,
59
- recorded: Option<DisplayBackend>,
60
- ) -> ResolvedBackend {
61
- let backend = requested.or(recorded).unwrap_or(DisplayBackend::Adaptive);
62
- ResolvedBackend {
63
- backend,
64
- non_default: backend != DisplayBackend::Adaptive,
65
- }
66
- }
67
-
68
- /// ---
69
- /// purpose: 探测当前环境是否具备 adaptive 显示能力
70
- /// params:
71
- /// _workspace: 未参与判定,仅保留签名
72
- /// returns: 平台名、leader 所在 tmux session 与 pane、能力位、状态与被挡原因
73
- /// errors: 当前实现不产生 Err
74
- /// ---
75
- ///
76
- /// `probe_display_capabilities(...)`(`display/adaptive.py:31`,C13)。能力探测,分支只看
77
- /// 结果不看 `cfg!(target_os)`;Windows/WSL → `NotImplementedThisPlatform`。
78
- pub fn probe_display_capabilities(_workspace: &Path) -> Result<DisplayProbe, LifecycleError> {
79
- let platform = std::env::consts::OS.to_string();
80
- let platform_supported = !matches!(platform.as_str(), "windows") && !in_wsl();
81
- let live_tmux_info = current_leader_tmux_info();
82
- let in_tmux = platform_supported && (running_inside_tmux() || live_tmux_info.is_some());
83
- let tmux_info = live_tmux_info.or_else(env_leader_tmux_info);
84
- let opened = in_tmux && platform_supported;
85
- let reason = if opened {
86
- None
87
- } else if platform_supported {
88
- Some(AdaptiveBlockReason::LeaderNotInTmux)
89
- } else {
90
- Some(AdaptiveBlockReason::NotImplementedThisPlatform)
91
- };
92
- Ok(DisplayProbe {
93
- in_tmux,
94
- platform,
95
- leader_session: tmux_info
96
- .as_ref()
97
- .map(|info| SessionName::new(info.session.clone())),
98
- leader_pane: tmux_info.and_then(|info| info.pane.map(PaneId::new)),
99
- caps: CapsFlags {
100
- tmux_append_windows: opened,
101
- adaptive_display: opened,
102
- },
103
- adaptive_status: if opened {
104
- DisplayStatus::Opened
105
- } else {
106
- DisplayStatus::Blocked
107
- },
108
- reason,
109
- })
110
- }
111
-
112
- /// ---
113
- /// purpose: 按已解析的后端为各 worker 打开显示
114
- /// params:
115
- /// backend: 已解析的显示后端;不带 worker 视图的后端得到空映射
116
- /// probe: 能力探测结果,adaptive 分支据此决定开还是记 blocked
117
- /// returns: 后端与 agent_id 到 WorkerDisplay 的映射
118
- /// errors: adaptive 开窗或读取显示目标失败时返回 LifecycleError
119
- /// ---
120
- ///
121
- /// `open_worker_displays(workspace, session_name, jobs, backend, capability_probe)`
122
- /// (`display/worker_window.py`)。按后端分派 adaptive / ghostty_workspace / ghostty_window;
123
- /// 显示失败**不阻塞** team readiness(C14)。
124
- pub(crate) fn open_worker_displays(
125
- workspace: &Path,
126
- session_name: &SessionName,
127
- backend: DisplayBackend,
128
- probe: &DisplayProbe,
129
- ) -> Result<OpenDisplaysReport, LifecycleError> {
130
- let displays = match backend {
131
- DisplayBackend::Adaptive => open_adaptive_worker_displays(workspace, session_name, probe)?,
132
- backend if backend.has_worker_views() => worker_display_targets(workspace, session_name)?
133
- .into_iter()
134
- .map(|target| {
135
- let display = display_for_target(&target, backend, probe);
136
- (target.agent_id, display)
137
- })
138
- .collect(),
139
- _ => BTreeMap::new(),
140
- };
141
- Ok(OpenDisplaysReport { backend, displays })
142
- }
143
-
144
- /// ---
145
- /// purpose: 关闭本 team 的显示,当前实现只处理 adaptive
146
- /// returns: 关闭报告
147
- /// errors: 底层 tmux 操作失败时返回 LifecycleError
148
- /// ---
149
- ///
150
- /// `close_team_display_backends(state, event_log)`(`display/close.py`,C9)。按 state
151
- /// 记录的后端关显示;adaptive 只删带 team tag 的窗口(C2 leader pane 安全)+ orphan 清理。
152
- pub fn close_team_display_backends(
153
- workspace: &Path,
154
- session_name: &SessionName,
155
- ) -> Result<CloseDisplaysReport, LifecycleError> {
156
- close_adaptive_displays(workspace, session_name)
157
- }
158
-
159
- /// ---
160
- /// purpose: leader rebind 完成之后重建 adaptive worker 窗口
161
- /// returns: 重新打开的显示映射
162
- /// errors: 同 open_worker_displays
163
- /// ---
164
- ///
165
- /// `rebuild_adaptive_display_after_rebind(...)`(`display/rebuild.py`,C12)。restart 在
166
- /// leader rebind **之后**重建 adaptive 窗口(读 `leader_receiver.rebind_applied` 事件)。
167
- pub fn rebuild_adaptive_display_after_rebind(
168
- workspace: &Path,
169
- session_name: &SessionName,
170
- probe: &DisplayProbe,
171
- ) -> Result<OpenDisplaysReport, LifecycleError> {
172
- open_worker_displays(workspace, session_name, DisplayBackend::Adaptive, probe)
173
- }
174
-
175
- #[derive(Debug, Clone)]
176
- struct DisplayTarget {
177
- agent_id: String,
178
- window: Option<WindowName>,
179
- pane_id: Option<PaneId>,
180
- role: Option<String>,
181
- }
182
-
183
- fn worker_display_targets(
184
- workspace: &Path,
185
- session_name: &SessionName,
186
- ) -> Result<Vec<DisplayTarget>, LifecycleError> {
187
- let state = match crate::state::persist::load_runtime_state(workspace) {
188
- Ok(state) => state,
189
- Err(_) => return Ok(Vec::new()),
190
- };
191
- let Some(agents) = state.get("agents").and_then(serde_json::Value::as_object) else {
192
- return Ok(Vec::new());
193
- };
194
- let mut targets = Vec::new();
195
- for (agent_id, agent) in agents {
196
- let window = agent
197
- .get("window")
198
- .and_then(serde_json::Value::as_str)
199
- .unwrap_or(agent_id);
200
- targets.push(DisplayTarget {
201
- agent_id: agent_id.clone(),
202
- window: Some(WindowName::new(window)),
203
- pane_id: agent
204
- .get("pane_id")
205
- .and_then(serde_json::Value::as_str)
206
- .filter(|pane| !pane.is_empty())
207
- .map(PaneId::new),
208
- role: agent
209
- .get("role")
210
- .and_then(serde_json::Value::as_str)
211
- .map(str::to_string),
212
- });
213
- }
214
- let _ = session_name;
215
- targets.sort_by(|a, b| a.agent_id.cmp(&b.agent_id));
216
- Ok(targets)
217
- }
218
-
219
- fn display_for_target(
220
- target: &DisplayTarget,
221
- backend: DisplayBackend,
222
- probe: &DisplayProbe,
223
- ) -> WorkerDisplay {
224
- match backend {
225
- DisplayBackend::Adaptive if probe.adaptive_status == DisplayStatus::Opened => {
226
- WorkerDisplay::Adaptive {
227
- status: DisplayStatus::Opened,
228
- window: target.window.clone(),
229
- workspace_window: target.window.clone(),
230
- pane_id: None,
231
- pane_title: target.window.as_ref().map(|_| display_pane_title(target)),
232
- target: target
233
- .window
234
- .as_ref()
235
- .map(|window| window.as_str().to_string()),
236
- target_worker_session: target
237
- .window
238
- .as_ref()
239
- .map(|window| window.as_str().to_string()),
240
- linked_session: None,
241
- leader_session: probe.leader_session.clone(),
242
- display_session: probe.leader_session.clone(),
243
- fallback: Some("tmux_headless".to_string()),
244
- }
245
- }
246
- DisplayBackend::Adaptive => WorkerDisplay::Blocked {
247
- reason: probe
248
- .reason
249
- .unwrap_or(AdaptiveBlockReason::AggregatorRebuildFailed),
250
- },
251
- DisplayBackend::Ghostty
252
- | DisplayBackend::GhosttyWindow
253
- | DisplayBackend::GhosttyWorkspace => WorkerDisplay::Blocked {
254
- reason: AdaptiveBlockReason::NotImplementedThisPlatform,
255
- },
256
- DisplayBackend::None | DisplayBackend::TmuxAttach | DisplayBackend::Iterm => {
257
- WorkerDisplay::Blocked {
258
- reason: AdaptiveBlockReason::NotImplementedThisPlatform,
259
- }
260
- }
261
- }
262
- }
263
-
264
- fn open_adaptive_worker_displays(
265
- workspace: &Path,
266
- session_name: &SessionName,
267
- probe: &DisplayProbe,
268
- ) -> Result<BTreeMap<String, WorkerDisplay>, LifecycleError> {
269
- let targets = worker_display_targets(workspace, session_name)?;
270
- Ok(targets
271
- .into_iter()
272
- .map(|target| {
273
- let agent_id = target.agent_id.clone();
274
- (
275
- agent_id.clone(),
276
- WorkerDisplay::Adaptive {
277
- status: probe.adaptive_status,
278
- window: target.window.clone(),
279
- workspace_window: target.window.clone(),
280
- pane_id: target.pane_id.clone(),
281
- pane_title: Some(agent_id),
282
- target: target
283
- .pane_id
284
- .as_ref()
285
- .map(|pane| pane.as_str().to_string()),
286
- target_worker_session: Some(session_name.as_str().to_string()),
287
- linked_session: None,
288
- leader_session: Some(session_name.clone()),
289
- display_session: Some(session_name.clone()),
290
- fallback: None,
291
- },
292
- )
293
- })
294
- .collect())
295
- }
296
-
297
- fn close_adaptive_displays(
298
- workspace: &Path,
299
- session_name: &SessionName,
300
- ) -> Result<CloseDisplaysReport, LifecycleError> {
301
- let state = match crate::state::persist::load_runtime_state(workspace) {
302
- Ok(state) => state,
303
- Err(_) => {
304
- return Ok(CloseDisplaysReport {
305
- closed: Vec::new(),
306
- orphans_cleaned: Vec::new(),
307
- })
308
- }
309
- };
310
- let Some(agents) = state.get("agents").and_then(serde_json::Value::as_object) else {
311
- return Ok(CloseDisplaysReport {
312
- closed: Vec::new(),
313
- orphans_cleaned: Vec::new(),
314
- });
315
- };
316
- // 0.5.39 Slice 1: build workspace-scoped tmux transport once and
317
- // route every destructive tmux op below through it (no ambient
318
- // $TMUX). The kill_* Transport methods carry the workspace socket
319
- // (`tmux -L <socket>`) so display cleanup cannot cross-kill a session
320
- // on the user's default tmux server.
321
- let transport = crate::transport_factory::tmux_workspace_transport(workspace);
322
- let mut closed = Vec::new();
323
- let mut orphans_cleaned = Vec::new();
324
- let mut seen = std::collections::BTreeSet::new();
325
- for (agent_id, agent) in agents {
326
- let Some(display) = agent.get("display").and_then(serde_json::Value::as_object) else {
327
- continue;
328
- };
329
- if display.get("backend").and_then(serde_json::Value::as_str) != Some("adaptive") {
330
- continue;
331
- }
332
- if display
333
- .get("status")
334
- .and_then(serde_json::Value::as_str)
335
- .is_some_and(|status| status.eq_ignore_ascii_case("stopped"))
336
- {
337
- continue;
338
- }
339
- for target in display_identifiers(display, agent, agent_id, session_name, &state) {
340
- if seen.insert(target.clone()) {
341
- kill_display_target(&transport, &target);
342
- closed.push(target);
343
- }
344
- }
345
- }
346
- if let Some(leader_session) = adaptive_leader_session(&state) {
347
- for target in
348
- close_adaptive_windows(&transport, leader_session.as_str(), session_name.as_str())
349
- {
350
- orphans_cleaned.push(target.clone());
351
- if seen.insert(target.clone()) {
352
- closed.push(target);
353
- }
354
- }
355
- }
356
- closed.sort();
357
- if orphans_cleaned.is_empty() {
358
- let overview_prefix = format!(":team-agent:{}:overview", session_name.as_str());
359
- orphans_cleaned.extend(
360
- closed
361
- .iter()
362
- .filter(|target| target.contains(&overview_prefix))
363
- .cloned(),
364
- );
365
- }
366
- orphans_cleaned.sort();
367
- orphans_cleaned.dedup();
368
- Ok(CloseDisplaysReport {
369
- closed,
370
- orphans_cleaned,
371
- })
372
- }
373
-
374
- fn display_identifiers(
375
- display: &serde_json::Map<String, serde_json::Value>,
376
- agent: &serde_json::Value,
377
- agent_id: &str,
378
- session_name: &SessionName,
379
- state: &serde_json::Value,
380
- ) -> Vec<String> {
381
- let mut targets = Vec::new();
382
- let leader_session = display
383
- .get("leader_session")
384
- .and_then(serde_json::Value::as_str)
385
- .filter(|s| !s.is_empty())
386
- .map(str::to_string)
387
- .or_else(|| adaptive_leader_session(state));
388
- let window = display
389
- .get("workspace_window")
390
- .or_else(|| display.get("window"))
391
- .or_else(|| agent.get("window"))
392
- .and_then(serde_json::Value::as_str)
393
- .filter(|s| !s.is_empty());
394
- if let (Some(leader_session), Some(window)) = (leader_session, window) {
395
- if adaptive_window_is_tagged(session_name.as_str(), window) {
396
- targets.push(format!("{leader_session}:{window}"));
397
- } else {
398
- targets.push(format!(
399
- "{leader_session}:{}",
400
- adaptive_window_name(session_name.as_str(), 0)
401
- ));
402
- }
403
- }
404
- for key in ["linked_session", "pane_id"] {
405
- if let Some(value) = string_field(display, key) {
406
- targets.push(value);
407
- }
408
- }
409
- if targets.is_empty() {
410
- let window = agent
411
- .get("window")
412
- .and_then(serde_json::Value::as_str)
413
- .filter(|s| !s.is_empty())
414
- .unwrap_or(agent_id);
415
- targets.push(format!("{}:{window}", session_name.as_str()));
416
- }
417
- targets
418
- }
419
-
420
- fn blocked_displays(
421
- targets: Vec<DisplayTarget>,
422
- reason: AdaptiveBlockReason,
423
- ) -> BTreeMap<String, WorkerDisplay> {
424
- targets
425
- .into_iter()
426
- .map(|target| (target.agent_id, WorkerDisplay::Blocked { reason }))
427
- .collect()
428
- }
429
-
430
- #[derive(Debug, Clone)]
431
- struct LeaderTmuxInfo {
432
- session: String,
433
- pane: Option<String>,
434
- }
435
-
436
- fn in_wsl() -> bool {
437
- std::env::var("WSL_DISTRO_NAME").is_ok_and(|value| !value.is_empty())
438
- || std::env::var("WSL_INTEROP").is_ok_and(|value| !value.is_empty())
439
- }
440
-
441
- fn running_inside_tmux() -> bool {
442
- std::env::var("TMUX").is_ok_and(|value| !value.is_empty())
443
- || std::env::var("TMUX_PANE").is_ok_and(|value| !value.is_empty())
444
- }
445
-
446
- fn current_leader_tmux_info() -> Option<LeaderTmuxInfo> {
447
- // 0.5.39 Slice 1: ambient probe lives in `tmux_backend` — the single
448
- // controlled exception to the "all tmux ops through socket-scoped
449
- // transport" rule (its whole job is "which session am I in?").
450
- crate::tmux_backend::probe_ambient_leader_pane_info()
451
- .map(|(session, pane)| LeaderTmuxInfo { session, pane })
452
- }
453
-
454
- fn env_leader_tmux_info() -> Option<LeaderTmuxInfo> {
455
- let session = std::env::var("TEAM_AGENT_LEADER_SESSION_NAME")
456
- .ok()
457
- .filter(|value| !value.is_empty())?;
458
- let pane = std::env::var("TEAM_AGENT_LEADER_PANE_ID")
459
- .ok()
460
- .or_else(|| std::env::var("TMUX_PANE").ok())
461
- .filter(|value| !value.is_empty());
462
- Some(LeaderTmuxInfo { session, pane })
463
- }
464
-
465
- fn close_adaptive_windows(
466
- transport: &dyn Transport,
467
- leader_session: &str,
468
- session_name: &str,
469
- ) -> Vec<String> {
470
- let prefix = format!("team-agent:{session_name}:overview");
471
- // 0.5.39 Slice 1: `Transport::list_windows` uses the workspace socket
472
- // — if `leader_session` doesn't exist on that socket, list_windows
473
- // returns Err/empty and we bail without touching another server.
474
- let Ok(windows) = transport.list_windows(&SessionName::new(leader_session.to_string())) else {
475
- return Vec::new();
476
- };
477
- windows
478
- .into_iter()
479
- .filter_map(|window| {
480
- let window = window.as_str().to_string();
481
- if window == prefix || window.starts_with(&format!("{prefix}-")) {
482
- let target_string = format!("{leader_session}:{window}");
483
- let target = Target::SessionWindow {
484
- session: SessionName::new(leader_session.to_string()),
485
- window: WindowName::new(window),
486
- };
487
- transport.kill_window(&target).ok().map(|_| target_string)
488
- } else {
489
- None
490
- }
491
- })
492
- .collect()
493
- }
494
-
495
- fn kill_display_target(transport: &dyn Transport, target: &str) {
496
- // 0.5.39 Slice 1: only kill window/pane — never fall back to
497
- // kill-session on an ambiguous string shape. Locate §7 Slice 1:
498
- // "Remove the string-shape `kill-session` fallback unless target is a
499
- // proven display-only session." The old code parsed any bare
500
- // non-`%`/non-`:` target as a session and issued `kill-session`,
501
- // which is exactly the unbounded blast radius we're closing.
502
- if let Some((session, window)) = target.split_once(':') {
503
- let _ = transport.kill_window(&Target::SessionWindow {
504
- session: SessionName::new(session.to_string()),
505
- window: WindowName::new(window.to_string()),
506
- });
507
- } else if target.starts_with('%') {
508
- let _ = transport.kill_pane(&PaneId::new(target.to_string()));
509
- }
510
- // Bare session-name form is intentionally a no-op: display cleanup
511
- // must not kill a whole session on shape guesswork.
512
- }
513
-
514
- fn adaptive_leader_session(state: &serde_json::Value) -> Option<String> {
515
- state
516
- .get("leader_receiver")
517
- .and_then(|receiver| receiver.get("session_name"))
518
- .and_then(serde_json::Value::as_str)
519
- .filter(|s| !s.is_empty())
520
- .map(str::to_string)
521
- }
522
-
523
- fn adaptive_window_name(session_name: &str, index: usize) -> String {
524
- if index == 0 {
525
- format!("team-agent:{session_name}:overview")
526
- } else {
527
- format!("team-agent:{session_name}:overview-{}", index + 1)
528
- }
529
- }
530
-
531
- fn adaptive_window_is_tagged(session_name: &str, window: &str) -> bool {
532
- let prefix = format!("team-agent:{session_name}:overview");
533
- window == prefix || window.starts_with(&format!("{prefix}-"))
534
- }
535
-
536
- fn display_pane_title(target: &DisplayTarget) -> String {
537
- format!(
538
- "team-agent:{}:{}",
539
- target.agent_id,
540
- target.role.as_deref().unwrap_or("")
541
- )
542
- }
543
-
544
- fn string_field(display: &serde_json::Map<String, serde_json::Value>, key: &str) -> Option<String> {
545
- display
546
- .get(key)
547
- .and_then(serde_json::Value::as_str)
548
- .filter(|s| !s.is_empty())
549
- .map(str::to_string)
550
- }