@team-agent/installer 0.3.1 → 0.3.3
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 +34 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +234 -26
- package/crates/team-agent/src/cli/diagnose.rs +144 -10
- package/crates/team-agent/src/cli/emit.rs +289 -54
- package/crates/team-agent/src/cli/leader.rs +37 -8
- package/crates/team-agent/src/cli/mod.rs +1281 -196
- package/crates/team-agent/src/cli/status_port.rs +195 -46
- package/crates/team-agent/src/cli/tests/divergence.rs +1 -2
- package/crates/team-agent/src/cli/tests/lane_c.rs +23 -13
- package/crates/team-agent/src/cli/tests/main_preserved.rs +2 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +59 -3
- package/crates/team-agent/src/cli/types.rs +18 -0
- package/crates/team-agent/src/compiler.rs +15 -5
- package/crates/team-agent/src/coordinator/health.rs +95 -17
- package/crates/team-agent/src/coordinator/mod.rs +4 -0
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +500 -0
- package/crates/team-agent/src/coordinator/runtime_observation.rs +58 -0
- package/crates/team-agent/src/coordinator/tick.rs +222 -69
- package/crates/team-agent/src/coordinator/types.rs +15 -3
- package/crates/team-agent/src/db/schema.rs +37 -2
- package/crates/team-agent/src/diagnose/comms.rs +226 -0
- package/crates/team-agent/src/diagnose/mod.rs +45 -0
- package/crates/team-agent/src/diagnose/orphans.rs +658 -0
- package/crates/team-agent/src/fake_worker.rs +146 -3
- package/crates/team-agent/src/leader/start.rs +121 -23
- package/crates/team-agent/src/leader/types.rs +44 -1
- package/crates/team-agent/src/lib.rs +3 -0
- package/crates/team-agent/src/lifecycle/display.rs +645 -47
- package/crates/team-agent/src/lifecycle/launch.rs +1061 -146
- package/crates/team-agent/src/lifecycle/mod.rs +2 -0
- package/crates/team-agent/src/lifecycle/profile_launch.rs +810 -0
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +522 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +99 -23
- package/crates/team-agent/src/lifecycle/restart/common.rs +183 -24
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +498 -22
- package/crates/team-agent/src/lifecycle/restart/remove.rs +27 -7
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
- package/crates/team-agent/src/lifecycle/restart.rs +24 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +5 -5
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +37 -7
- package/crates/team-agent/src/lifecycle/types.rs +19 -0
- package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +341 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +10 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +158 -0
- package/crates/team-agent/src/mcp_server/mod.rs +3 -74
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/send.rs +6 -5
- package/crates/team-agent/src/mcp_server/tools.rs +312 -111
- package/crates/team-agent/src/mcp_server/types.rs +6 -4
- package/crates/team-agent/src/mcp_server/wire.rs +19 -7
- package/crates/team-agent/src/message_store.rs +21 -4
- package/crates/team-agent/src/messaging/delivery.rs +470 -59
- package/crates/team-agent/src/messaging/mod.rs +9 -6
- package/crates/team-agent/src/messaging/results.rs +353 -63
- package/crates/team-agent/src/messaging/selftest.rs +199 -12
- package/crates/team-agent/src/messaging/send.rs +35 -3
- package/crates/team-agent/src/messaging/tests/runtime.rs +19 -4
- package/crates/team-agent/src/messaging/types.rs +11 -3
- package/crates/team-agent/src/os_probe.rs +119 -0
- package/crates/team-agent/src/packaging/migrate.rs +10 -2
- package/crates/team-agent/src/packaging/tests.rs +23 -0
- package/crates/team-agent/src/provider/adapter.rs +564 -63
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +1 -7
- package/crates/team-agent/src/provider/classify.rs +51 -4
- package/crates/team-agent/src/provider/helpers.rs +10 -1
- package/crates/team-agent/src/provider/startup_prompt.rs +94 -0
- package/crates/team-agent/src/provider/types.rs +47 -0
- package/crates/team-agent/src/session_capture.rs +616 -0
- package/crates/team-agent/src/state/persist.rs +170 -1
- package/crates/team-agent/src/state/projection.rs +141 -8
- package/crates/team-agent/src/state/selector.rs +5 -2
- package/crates/team-agent/src/tmux_backend.rs +161 -64
- package/crates/team-agent/src/transport/test_support.rs +9 -0
- package/crates/team-agent/src/transport/tests/wire.rs +4 -0
- package/crates/team-agent/src/transport.rs +13 -2
- package/package.json +4 -4
|
@@ -64,18 +64,19 @@ pub fn run(
|
|
|
64
64
|
.and_then(serde_json::Value::as_str)
|
|
65
65
|
.unwrap_or("unknown");
|
|
66
66
|
let task_id = payload.get("task_id").and_then(serde_json::Value::as_str);
|
|
67
|
-
report_fake_result(workspace, agent_id, message_id, task_id, &mut output)?;
|
|
67
|
+
report_fake_result(workspace, agent_id, message_id, task_id, None, &mut output)?;
|
|
68
68
|
block = None;
|
|
69
69
|
} else if let Some(parsed) = parse_rendered_header(trimmed) {
|
|
70
70
|
block = Some(parsed);
|
|
71
71
|
} else if let Some(token) = parse_token(trimmed) {
|
|
72
72
|
if let Some(current) = block.take() {
|
|
73
|
-
let
|
|
73
|
+
let content = current.content();
|
|
74
74
|
report_fake_result(
|
|
75
75
|
workspace,
|
|
76
76
|
agent_id,
|
|
77
77
|
&token,
|
|
78
78
|
current.task_id.as_deref(),
|
|
79
|
+
Some(&content),
|
|
79
80
|
&mut output,
|
|
80
81
|
)?;
|
|
81
82
|
}
|
|
@@ -126,15 +127,157 @@ fn report_fake_result(
|
|
|
126
127
|
agent_id: &str,
|
|
127
128
|
message_id: &str,
|
|
128
129
|
task_id: Option<&str>,
|
|
130
|
+
source_content: Option<&str>,
|
|
129
131
|
output: &mut impl Write,
|
|
130
132
|
) -> Result<(), FakeWorkerError> {
|
|
131
133
|
let envelope = fake_envelope(workspace, agent_id, message_id, task_id);
|
|
132
|
-
|
|
134
|
+
let owner_team = std::env::var("TEAM_AGENT_OWNER_TEAM_ID")
|
|
135
|
+
.ok()
|
|
136
|
+
.map(|value| value.trim().to_string())
|
|
137
|
+
.filter(|value| !value.is_empty());
|
|
138
|
+
mirror_fake_result_to_leader(workspace, owner_team.as_deref(), &envelope, source_content);
|
|
139
|
+
crate::messaging::report_result_for_owner_team(workspace, &envelope, owner_team.as_deref())
|
|
133
140
|
.map_err(|e| FakeWorkerError::Report(e.to_string()))?;
|
|
141
|
+
mirror_fake_result_to_leader(workspace, owner_team.as_deref(), &envelope, source_content);
|
|
134
142
|
writeln!(output, "{}", serde_json::to_string(&envelope)?)?;
|
|
135
143
|
Ok(())
|
|
136
144
|
}
|
|
137
145
|
|
|
146
|
+
fn mirror_fake_result_to_leader(
|
|
147
|
+
workspace: &Path,
|
|
148
|
+
owner_team: Option<&str>,
|
|
149
|
+
envelope: &serde_json::Value,
|
|
150
|
+
source_content: Option<&str>,
|
|
151
|
+
) {
|
|
152
|
+
let Ok(raw_state) = crate::state::persist::load_runtime_state(workspace) else {
|
|
153
|
+
return;
|
|
154
|
+
};
|
|
155
|
+
let state = owner_team
|
|
156
|
+
.and_then(|team| crate::state::projection::resolve_owner_team_id(&raw_state, team).canonical_key().map(str::to_string))
|
|
157
|
+
.map(|team| crate::state::projection::project_top_level_view(&raw_state, &team))
|
|
158
|
+
.unwrap_or(raw_state);
|
|
159
|
+
let attached = state
|
|
160
|
+
.get("leader_receiver")
|
|
161
|
+
.and_then(|receiver| receiver.get("status"))
|
|
162
|
+
.and_then(serde_json::Value::as_str)
|
|
163
|
+
== Some("attached");
|
|
164
|
+
let pane_id = state
|
|
165
|
+
.get("leader_receiver")
|
|
166
|
+
.and_then(|receiver| receiver.get("pane_id"))
|
|
167
|
+
.and_then(serde_json::Value::as_str)
|
|
168
|
+
.filter(|pane| !pane.is_empty())
|
|
169
|
+
.map(str::to_string)
|
|
170
|
+
.or_else(|| owner_team.and_then(|team| claimed_pane_from_events(workspace, team)));
|
|
171
|
+
let Some(pane_id) = pane_id.filter(|_| attached || owner_team.is_some()) else {
|
|
172
|
+
return;
|
|
173
|
+
};
|
|
174
|
+
let summary = envelope
|
|
175
|
+
.get("summary")
|
|
176
|
+
.and_then(serde_json::Value::as_str)
|
|
177
|
+
.unwrap_or("Fake worker completed");
|
|
178
|
+
let result_id = envelope
|
|
179
|
+
.get("result_id")
|
|
180
|
+
.and_then(serde_json::Value::as_str)
|
|
181
|
+
.unwrap_or("");
|
|
182
|
+
let text = match source_content.filter(|content| !content.trim().is_empty()) {
|
|
183
|
+
Some(content) => format!(
|
|
184
|
+
"Fake worker result: {summary}\nMessage content: {}\nResult id: {result_id}",
|
|
185
|
+
content.trim()
|
|
186
|
+
),
|
|
187
|
+
None => format!("Fake worker result: {summary}\nResult id: {result_id}"),
|
|
188
|
+
};
|
|
189
|
+
let target = crate::transport::Target::Pane(crate::transport::PaneId::new(&pane_id));
|
|
190
|
+
let payload = crate::transport::InjectPayload::Text(text.clone());
|
|
191
|
+
let key = crate::transport::Key::Enter;
|
|
192
|
+
if let Some(socket) = leader_receiver_full_socket(&state) {
|
|
193
|
+
let endpoint_backend = crate::tmux_backend::TmuxBackend::for_tmux_endpoint(socket);
|
|
194
|
+
if crate::transport::Transport::inject(&endpoint_backend, &target, &payload, key, true).is_ok() {
|
|
195
|
+
mirror_to_team_session_panes(&state, workspace, agent_id_from_envelope(envelope), &text);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
let workspace_backend = crate::tmux_backend::TmuxBackend::for_workspace(workspace);
|
|
200
|
+
let _ = crate::transport::Transport::inject(&workspace_backend, &target, &payload, key, true);
|
|
201
|
+
mirror_to_team_session_panes(&state, workspace, agent_id_from_envelope(envelope), &text);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
fn leader_receiver_full_socket(state: &serde_json::Value) -> Option<&str> {
|
|
205
|
+
state
|
|
206
|
+
.get("leader_receiver")
|
|
207
|
+
.and_then(|receiver| receiver.get("tmux_socket"))
|
|
208
|
+
.and_then(serde_json::Value::as_str)
|
|
209
|
+
.filter(|socket| !socket.is_empty() && Path::new(socket).is_absolute())
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
fn claimed_pane_from_events(workspace: &Path, owner_team: &str) -> Option<String> {
|
|
213
|
+
crate::event_log::EventLog::new(workspace)
|
|
214
|
+
.tail(50)
|
|
215
|
+
.ok()?
|
|
216
|
+
.into_iter()
|
|
217
|
+
.rev()
|
|
218
|
+
.find_map(|event| {
|
|
219
|
+
if event.get("event").and_then(serde_json::Value::as_str)
|
|
220
|
+
!= Some("leader_receiver.rebind_applied")
|
|
221
|
+
{
|
|
222
|
+
return None;
|
|
223
|
+
}
|
|
224
|
+
if event.get("team_id").and_then(serde_json::Value::as_str) != Some(owner_team) {
|
|
225
|
+
return None;
|
|
226
|
+
}
|
|
227
|
+
event
|
|
228
|
+
.get("new_pane_id")
|
|
229
|
+
.and_then(serde_json::Value::as_str)
|
|
230
|
+
.filter(|pane| !pane.is_empty())
|
|
231
|
+
.map(str::to_string)
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
fn agent_id_from_envelope(envelope: &serde_json::Value) -> Option<&str> {
|
|
236
|
+
envelope.get("agent_id").and_then(serde_json::Value::as_str)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
fn mirror_to_team_session_panes(
|
|
240
|
+
state: &serde_json::Value,
|
|
241
|
+
workspace: &Path,
|
|
242
|
+
agent_id: Option<&str>,
|
|
243
|
+
text: &str,
|
|
244
|
+
) {
|
|
245
|
+
let Some(session) = state
|
|
246
|
+
.get("session_name")
|
|
247
|
+
.and_then(serde_json::Value::as_str)
|
|
248
|
+
.filter(|session| !session.is_empty())
|
|
249
|
+
else {
|
|
250
|
+
return;
|
|
251
|
+
};
|
|
252
|
+
let worker_pane = agent_id.and_then(|agent_id| {
|
|
253
|
+
state
|
|
254
|
+
.get("agents")
|
|
255
|
+
.and_then(|agents| agents.get(agent_id))
|
|
256
|
+
.and_then(|agent| agent.get("pane_id"))
|
|
257
|
+
.and_then(serde_json::Value::as_str)
|
|
258
|
+
});
|
|
259
|
+
if let Some(socket) = leader_receiver_full_socket(state) {
|
|
260
|
+
let backend = crate::tmux_backend::TmuxBackend::for_tmux_endpoint(socket);
|
|
261
|
+
for pane in crate::transport::Transport::list_targets(&backend).unwrap_or_default() {
|
|
262
|
+
if pane.session.as_str() != session || Some(pane.pane_id.as_str()) == worker_pane {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
let target = crate::transport::Target::Pane(pane.pane_id);
|
|
266
|
+
let payload = crate::transport::InjectPayload::Text(text.to_string());
|
|
267
|
+
let _ = crate::transport::Transport::inject(&backend, &target, &payload, crate::transport::Key::Enter, true);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
let backend = crate::tmux_backend::TmuxBackend::for_workspace(workspace);
|
|
271
|
+
for pane in crate::transport::Transport::list_targets(&backend).unwrap_or_default() {
|
|
272
|
+
if pane.session.as_str() != session || Some(pane.pane_id.as_str()) == worker_pane {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
let target = crate::transport::Target::Pane(pane.pane_id);
|
|
276
|
+
let payload = crate::transport::InjectPayload::Text(text.to_string());
|
|
277
|
+
let _ = crate::transport::Transport::inject(&backend, &target, &payload, crate::transport::Key::Enter, true);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
138
281
|
fn fake_envelope(
|
|
139
282
|
workspace: &Path,
|
|
140
283
|
agent_id: &str,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
//! leader::start — leader_start_plan / start_leader / leader_session_name(派生 tmux session 名)。
|
|
2
2
|
|
|
3
3
|
use std::collections::BTreeMap;
|
|
4
|
+
use std::io::IsTerminal;
|
|
4
5
|
use std::path::Path;
|
|
5
|
-
use std::process::Command;
|
|
6
|
+
use std::process::{Command, Stdio};
|
|
6
7
|
|
|
7
8
|
use crate::provider::{get_adapter, Provider};
|
|
8
9
|
use crate::tmux_backend::TmuxBackend;
|
|
@@ -12,7 +13,10 @@ use super::helpers::{
|
|
|
12
13
|
provider_wire, resolve_workspace_for_hash, sanitize_session_folder, sha1_hex_prefix,
|
|
13
14
|
};
|
|
14
15
|
use super::owner_bind::leader_identity_context;
|
|
15
|
-
use super::{
|
|
16
|
+
use super::{
|
|
17
|
+
LeaderError, LeaderLaunchOutcome, LeaderLaunchSocket, LeaderLaunchStatus, LeaderStartMode,
|
|
18
|
+
LeaderStartPlan,
|
|
19
|
+
};
|
|
16
20
|
|
|
17
21
|
// ── leader::start — leader_start_plan / start_leader / session 名 ──
|
|
18
22
|
|
|
@@ -27,10 +31,14 @@ pub fn leader_start_plan(
|
|
|
27
31
|
attach_session: Option<&SessionName>,
|
|
28
32
|
) -> Result<LeaderStartPlan, LeaderError> {
|
|
29
33
|
if attach_session.is_some() && !confirm_attach {
|
|
30
|
-
return Err(LeaderError::Start(
|
|
34
|
+
return Err(LeaderError::Start(
|
|
35
|
+
"--attach-session requires --confirm".to_string(),
|
|
36
|
+
));
|
|
31
37
|
}
|
|
32
38
|
if attach_existing && !confirm_attach {
|
|
33
|
-
return Err(LeaderError::Start(
|
|
39
|
+
return Err(LeaderError::Start(
|
|
40
|
+
"attach existing leader session requires confirm".to_string(),
|
|
41
|
+
));
|
|
34
42
|
}
|
|
35
43
|
let adapter = get_adapter(provider);
|
|
36
44
|
if !adapter.is_installed() {
|
|
@@ -65,7 +73,10 @@ pub fn leader_start_plan(
|
|
|
65
73
|
LeaderStartMode::NewTmuxSession
|
|
66
74
|
};
|
|
67
75
|
let mut leader_env = BTreeMap::new();
|
|
68
|
-
leader_env.insert(
|
|
76
|
+
leader_env.insert(
|
|
77
|
+
"TEAM_AGENT_LEADER_PROVIDER".to_string(),
|
|
78
|
+
provider_wire(provider).to_string(),
|
|
79
|
+
);
|
|
69
80
|
leader_env.insert(
|
|
70
81
|
"TEAM_AGENT_LEADER_SESSION_UUID".to_string(),
|
|
71
82
|
identity.leader_session_uuid.as_str().to_string(),
|
|
@@ -78,8 +89,18 @@ pub fn leader_start_plan(
|
|
|
78
89
|
"TEAM_AGENT_WORKSPACE".to_string(),
|
|
79
90
|
identity.workspace_abspath.to_string_lossy().into_owned(),
|
|
80
91
|
);
|
|
81
|
-
leader_env.insert(
|
|
82
|
-
|
|
92
|
+
leader_env.insert(
|
|
93
|
+
"TEAM_AGENT_TEAM_ID".to_string(),
|
|
94
|
+
identity.team_id.as_str().to_string(),
|
|
95
|
+
);
|
|
96
|
+
let argv = start_argv(
|
|
97
|
+
mode,
|
|
98
|
+
provider,
|
|
99
|
+
provider_args,
|
|
100
|
+
workspace,
|
|
101
|
+
session_name.as_ref(),
|
|
102
|
+
&leader_env,
|
|
103
|
+
)?;
|
|
83
104
|
let plan_env = if mode == LeaderStartMode::ExecProvider {
|
|
84
105
|
merged_exec_env(&leader_env)
|
|
85
106
|
} else {
|
|
@@ -89,6 +110,7 @@ pub fn leader_start_plan(
|
|
|
89
110
|
mode,
|
|
90
111
|
provider,
|
|
91
112
|
workspace: resolve_workspace_for_hash(workspace),
|
|
113
|
+
socket: LeaderLaunchSocket::Workspace,
|
|
92
114
|
session_name,
|
|
93
115
|
argv,
|
|
94
116
|
leader_env: plan_env,
|
|
@@ -123,7 +145,46 @@ pub fn start_leader(
|
|
|
123
145
|
"session_name": plan.session_name.as_ref().map(|s| s.as_str().to_string()),
|
|
124
146
|
}),
|
|
125
147
|
)?;
|
|
126
|
-
|
|
148
|
+
execute_leader_plan(&plan, workspace).map(|_| ())
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// Execute a precomputed leader launch plan.
|
|
152
|
+
///
|
|
153
|
+
/// S0 exposes the seam and return model only. Lane 2 owns the real provider/tmux
|
|
154
|
+
/// execution and workspace-socket enforcement.
|
|
155
|
+
pub fn execute_leader_plan(
|
|
156
|
+
plan: &LeaderStartPlan,
|
|
157
|
+
workspace: &Path,
|
|
158
|
+
) -> Result<LeaderLaunchOutcome, LeaderError> {
|
|
159
|
+
let mut argv = plan.argv.clone();
|
|
160
|
+
let detached = plan.mode == LeaderStartMode::NewTmuxSession
|
|
161
|
+
&& !std::io::stdin().is_terminal()
|
|
162
|
+
&& insert_detach_flag(&mut argv);
|
|
163
|
+
let status = run_leader_argv(&argv, &plan.leader_env)?;
|
|
164
|
+
let code = status.code();
|
|
165
|
+
if !status.success() {
|
|
166
|
+
return Err(LeaderError::Start(format!(
|
|
167
|
+
"leader launcher exited with status {}",
|
|
168
|
+
code.map(|c| c.to_string())
|
|
169
|
+
.unwrap_or_else(|| "signal".to_string())
|
|
170
|
+
)));
|
|
171
|
+
}
|
|
172
|
+
if detached {
|
|
173
|
+
Ok(LeaderLaunchOutcome {
|
|
174
|
+
status: LeaderLaunchStatus::Detached,
|
|
175
|
+
exit_code: code,
|
|
176
|
+
session_name: plan.session_name.clone(),
|
|
177
|
+
reason: None,
|
|
178
|
+
})
|
|
179
|
+
} else {
|
|
180
|
+
let _ = workspace;
|
|
181
|
+
Ok(LeaderLaunchOutcome {
|
|
182
|
+
status: LeaderLaunchStatus::Exited,
|
|
183
|
+
exit_code: code,
|
|
184
|
+
session_name: plan.session_name.clone(),
|
|
185
|
+
reason: None,
|
|
186
|
+
})
|
|
187
|
+
}
|
|
127
188
|
}
|
|
128
189
|
|
|
129
190
|
/// `leader_session_name`(card §48;`__init__.py:186`)。确定派生 tmux session 名
|
|
@@ -161,12 +222,13 @@ fn start_argv(
|
|
|
161
222
|
let Some(session) = session_name else {
|
|
162
223
|
return Err(LeaderError::Start("attach session missing".to_string()));
|
|
163
224
|
};
|
|
164
|
-
|
|
225
|
+
let argv = vec![
|
|
165
226
|
"tmux".to_string(),
|
|
166
227
|
"attach-session".to_string(),
|
|
167
228
|
"-t".to_string(),
|
|
168
229
|
session.as_str().to_string(),
|
|
169
|
-
]
|
|
230
|
+
];
|
|
231
|
+
Ok(TmuxBackend::argv_for_workspace(workspace, &argv))
|
|
170
232
|
}
|
|
171
233
|
LeaderStartMode::NewTmuxSession => {
|
|
172
234
|
let Some(session) = session_name else {
|
|
@@ -185,7 +247,7 @@ fn start_argv(
|
|
|
185
247
|
exports.join(" "),
|
|
186
248
|
shell_join(&provider_argv)
|
|
187
249
|
);
|
|
188
|
-
|
|
250
|
+
let argv = vec![
|
|
189
251
|
"tmux".to_string(),
|
|
190
252
|
"new-session".to_string(),
|
|
191
253
|
"-s".to_string(),
|
|
@@ -197,11 +259,42 @@ fn start_argv(
|
|
|
197
259
|
"sh".to_string(),
|
|
198
260
|
"-lc".to_string(),
|
|
199
261
|
shell,
|
|
200
|
-
]
|
|
262
|
+
];
|
|
263
|
+
Ok(TmuxBackend::argv_for_workspace(workspace, &argv))
|
|
201
264
|
}
|
|
202
265
|
}
|
|
203
266
|
}
|
|
204
267
|
|
|
268
|
+
fn insert_detach_flag(argv: &mut Vec<String>) -> bool {
|
|
269
|
+
if argv.iter().any(|arg| arg == "-d") {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
let Some(pos) = argv.iter().position(|arg| arg == "new-session") else {
|
|
273
|
+
return false;
|
|
274
|
+
};
|
|
275
|
+
argv.insert(pos + 1, "-d".to_string());
|
|
276
|
+
true
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
fn run_leader_argv(
|
|
280
|
+
argv: &[String],
|
|
281
|
+
env: &BTreeMap<String, String>,
|
|
282
|
+
) -> Result<std::process::ExitStatus, LeaderError> {
|
|
283
|
+
let Some(program) = argv.first() else {
|
|
284
|
+
return Err(LeaderError::Start(
|
|
285
|
+
"leader launch argv is empty".to_string(),
|
|
286
|
+
));
|
|
287
|
+
};
|
|
288
|
+
let mut child = Command::new(program)
|
|
289
|
+
.args(argv.iter().skip(1))
|
|
290
|
+
.envs(env)
|
|
291
|
+
.stdin(Stdio::inherit())
|
|
292
|
+
.stdout(Stdio::inherit())
|
|
293
|
+
.stderr(Stdio::inherit())
|
|
294
|
+
.spawn()?;
|
|
295
|
+
child.wait().map_err(LeaderError::Io)
|
|
296
|
+
}
|
|
297
|
+
|
|
205
298
|
fn ensure_tmux_installed() -> Result<(), LeaderError> {
|
|
206
299
|
match Command::new("tmux").arg("-V").output() {
|
|
207
300
|
Ok(output) if output.status.success() => Ok(()),
|
|
@@ -246,25 +339,30 @@ fn leader_export_assignments(leader_env: &BTreeMap<String, String>) -> Vec<Strin
|
|
|
246
339
|
|
|
247
340
|
fn merged_exec_env(leader_env: &BTreeMap<String, String>) -> BTreeMap<String, String> {
|
|
248
341
|
let mut env: BTreeMap<String, String> = std::env::vars().collect();
|
|
249
|
-
env.extend(
|
|
342
|
+
env.extend(
|
|
343
|
+
leader_env
|
|
344
|
+
.iter()
|
|
345
|
+
.map(|(key, value)| (key.clone(), value.clone())),
|
|
346
|
+
);
|
|
250
347
|
env
|
|
251
348
|
}
|
|
252
349
|
|
|
253
350
|
fn shell_join(args: &[String]) -> String {
|
|
254
|
-
args.iter()
|
|
351
|
+
args.iter()
|
|
352
|
+
.map(|arg| shlex_quote(arg))
|
|
353
|
+
.collect::<Vec<_>>()
|
|
354
|
+
.join(" ")
|
|
255
355
|
}
|
|
256
356
|
|
|
257
357
|
fn shlex_quote(raw: &str) -> String {
|
|
258
358
|
if !raw.is_empty()
|
|
259
|
-
&& raw
|
|
260
|
-
.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
)
|
|
267
|
-
})
|
|
359
|
+
&& raw.bytes().all(|b| {
|
|
360
|
+
b.is_ascii_alphanumeric()
|
|
361
|
+
|| matches!(
|
|
362
|
+
b,
|
|
363
|
+
b'@' | b'%' | b'_' | b'+' | b'=' | b':' | b',' | b'.' | b'/' | b'-'
|
|
364
|
+
)
|
|
365
|
+
})
|
|
268
366
|
{
|
|
269
367
|
raw.to_string()
|
|
270
368
|
} else {
|
|
@@ -151,6 +151,24 @@ pub enum LeaderStartMode {
|
|
|
151
151
|
AttachExisting,
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
/// Leader launcher tmux/socket selection. Managed launcher sessions must use the
|
|
155
|
+
/// workspace tmux socket, never the user's default tmux server.
|
|
156
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
157
|
+
#[serde(rename_all = "snake_case")]
|
|
158
|
+
pub enum LeaderLaunchSocket {
|
|
159
|
+
Workspace,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// Execution status for a leader launch plan. `NotStarted` is intentionally
|
|
163
|
+
/// distinct so JSON callers cannot report `ok:true` for an unexecuted launcher.
|
|
164
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
165
|
+
#[serde(rename_all = "snake_case")]
|
|
166
|
+
pub enum LeaderLaunchStatus {
|
|
167
|
+
Exited,
|
|
168
|
+
Detached,
|
|
169
|
+
NotStarted,
|
|
170
|
+
}
|
|
171
|
+
|
|
154
172
|
/// 全部 leader 审计事件名(card §34)。§3 typed event kinds + §40 JSON 名与 Python
|
|
155
173
|
/// **字节级一致**。映射到 [`LeaderEvent::name`] 返回 `EventLog::write` 用的精确字符串。
|
|
156
174
|
/// (既有 `EventLog::write(&str, Value)` 仍吃裸字符串;此 enum 是 type-safe 名表,
|
|
@@ -200,7 +218,9 @@ impl LeaderEvent {
|
|
|
200
218
|
Self::ReceiverStateDivergenceRepaired => "leader_receiver.state_divergence_repaired",
|
|
201
219
|
Self::ReceiverFirstTimeEnvSeeded => "leader_receiver.first_time_env_seeded",
|
|
202
220
|
Self::ReceiverAutobindSkipped => "leader_receiver.autobind_skipped",
|
|
203
|
-
Self::ReceiverRequeuedExhaustedWatchers =>
|
|
221
|
+
Self::ReceiverRequeuedExhaustedWatchers => {
|
|
222
|
+
"leader_receiver.requeued_exhausted_watchers"
|
|
223
|
+
}
|
|
204
224
|
Self::ReceiverAmbiguousCandidates => "leader_receiver.ambiguous_candidates",
|
|
205
225
|
Self::ReceiverClaimRequeue => "leader_receiver.claim_requeue",
|
|
206
226
|
Self::ReceiverClaimLeaderNotification => "leader_receiver.claim_leader_notification",
|
|
@@ -307,6 +327,7 @@ pub struct LeaderStartPlan {
|
|
|
307
327
|
pub mode: LeaderStartMode,
|
|
308
328
|
pub provider: Provider,
|
|
309
329
|
pub workspace: PathBuf,
|
|
330
|
+
pub socket: LeaderLaunchSocket,
|
|
310
331
|
pub session_name: Option<SessionName>,
|
|
311
332
|
/// 要 exec 的 argv(provider argv 或 tmux argv)。
|
|
312
333
|
pub argv: Vec<String>,
|
|
@@ -317,6 +338,28 @@ pub struct LeaderStartPlan {
|
|
|
317
338
|
pub detached: bool,
|
|
318
339
|
}
|
|
319
340
|
|
|
341
|
+
/// Result of executing a [`LeaderStartPlan`]. Interactive launches should carry
|
|
342
|
+
/// the provider/tmux process exit code; detached launches carry the managed
|
|
343
|
+
/// session when known.
|
|
344
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
345
|
+
pub struct LeaderLaunchOutcome {
|
|
346
|
+
pub status: LeaderLaunchStatus,
|
|
347
|
+
pub exit_code: Option<i32>,
|
|
348
|
+
pub session_name: Option<SessionName>,
|
|
349
|
+
pub reason: Option<String>,
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
impl LeaderLaunchOutcome {
|
|
353
|
+
pub fn not_started(reason: impl Into<String>) -> Self {
|
|
354
|
+
Self {
|
|
355
|
+
status: LeaderLaunchStatus::NotStarted,
|
|
356
|
+
exit_code: None,
|
|
357
|
+
session_name: None,
|
|
358
|
+
reason: Some(reason.into()),
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
320
363
|
/// idle-takeover 的 node 分类行(`build_idle_nodes` / `_leader_node` 产物)。
|
|
321
364
|
/// **bug-085**:`state` 用 `TurnState`(穷尽,`Unknown` 不当 idle);`rollout_path` `Option`。
|
|
322
365
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
@@ -55,6 +55,8 @@ pub mod message_store;
|
|
|
55
55
|
// step 8 (provider) — ProviderAdapter trait + typed provider/turn-state/liveness 等(ROUND-0 骨架;
|
|
56
56
|
// fn body unimplemented!(),P2 porter 落实现)。MUST-NOT-13:provider 调用全走 trait。
|
|
57
57
|
pub mod provider;
|
|
58
|
+
pub mod session_capture;
|
|
59
|
+
pub(crate) mod os_probe;
|
|
58
60
|
|
|
59
61
|
// step 9 (transport) — Transport trait(控制面)+ Target/PaneId/InjectReport 等(ROUND-0 骨架;
|
|
60
62
|
// fn body unimplemented!(),P2 porter 落实现)。tmux/WezTerm/ConPTY 三后端。
|
|
@@ -66,6 +68,7 @@ pub mod transport;
|
|
|
66
68
|
pub mod leader;
|
|
67
69
|
pub mod messaging;
|
|
68
70
|
pub mod coordinator;
|
|
71
|
+
pub mod diagnose;
|
|
69
72
|
|
|
70
73
|
// step 13-15 (lifecycle/mcp_server/cli/packaging) — ROUND-0.5b behavioral-rich 骨架(entry-fn 签名 +
|
|
71
74
|
// 富返回类型,fn body unimplemented!(),P2 porter 落实现)。lifecycle=quick-start/restart/display;
|