@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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +0 -7
- package/crates/team-agent/src/cli/diagnose.rs +0 -16
- package/crates/team-agent/src/cli/emit.rs +15 -20
- package/crates/team-agent/src/cli/mod.rs +130 -31
- package/crates/team-agent/src/cli/spec.rs +7 -7
- package/crates/team-agent/src/cli/tests/main_preserved.rs +0 -1
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +0 -1
- package/crates/team-agent/src/cli/tests/run_delegation.rs +0 -3
- package/crates/team-agent/src/cli/tests/verb_validate.rs +0 -1
- package/crates/team-agent/src/cli/types.rs +0 -6
- package/crates/team-agent/src/compiler/tests.rs +5 -8
- package/crates/team-agent/src/compiler.rs +6 -14
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +5 -25
- package/crates/team-agent/src/lifecycle/launch/identity.rs +2 -18
- package/crates/team-agent/src/lifecycle/launch/layout.rs +7 -29
- package/crates/team-agent/src/lifecycle/launch/leader_context.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/ownership.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +7 -7
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +12 -49
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +3 -9
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +2 -61
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +2 -15
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +3 -7
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch.rs +3 -4
- package/crates/team-agent/src/lifecycle/mod.rs +0 -8
- package/crates/team-agent/src/lifecycle/restart/agent.rs +62 -8
- package/crates/team-agent/src/lifecycle/restart/common.rs +0 -32
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +0 -9
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +1 -2
- package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +0 -1
- package/crates/team-agent/src/lifecycle/tests/core.rs +0 -213
- package/crates/team-agent/src/lifecycle/tests/issue238_codex_effort_contract.rs +96 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +1 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +31 -77
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +4 -8
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +7 -7
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +3 -4
- package/crates/team-agent/src/lifecycle/tests.rs +1 -2
- package/crates/team-agent/src/lifecycle/types.rs +3 -133
- package/crates/team-agent/src/model/enums.rs +21 -71
- package/crates/team-agent/src/model/spec.rs +5 -20
- package/crates/team-agent/src/provider/adapters/codex.rs +2 -2
- package/crates/team-agent/src/provider/types.rs +6 -5
- package/examples/team.spec.yaml +0 -1
- package/package.json +4 -4
- package/schemas/team.schema.json +2 -2
- package/skills/team-agent/SKILL.md +1 -1
- package/skills/team-agent/references/team-agent-operator.md +3 -10
- package/crates/team-agent/src/lifecycle/display.rs +0 -550
- package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +0 -884
- 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
|
-
///
|
|
357
|
-
/// -
|
|
358
|
-
///
|
|
359
|
-
///
|
|
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
|
|
package/examples/team.spec.yaml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
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.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
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",
|
package/schemas/team.schema.json
CHANGED
|
@@ -59,11 +59,11 @@
|
|
|
59
59
|
},
|
|
60
60
|
"runtime": {
|
|
61
61
|
"type": "object",
|
|
62
|
-
"required": ["backend", "
|
|
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": {
|
|
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.
|
|
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`.
|
|
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
|
-
|
|
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] [--
|
|
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
|
-
}
|