@team-agent/installer 0.4.11 → 0.5.0
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 +1 -0
- package/crates/team-agent/src/cli/diagnose.rs +2 -15
- package/crates/team-agent/src/cli/emit.rs +62 -4
- package/crates/team-agent/src/compiler.rs +11 -20
- package/crates/team-agent/src/coordinator/orphan.rs +2 -2
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +5 -4
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +1776 -2
- package/crates/team-agent/src/coordinator/steps/mod.rs +19 -0
- package/crates/team-agent/src/coordinator/tests/a0_lostupdate.rs +78 -7
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +195 -0
- package/crates/team-agent/src/coordinator/tick.rs +31 -932
- package/crates/team-agent/src/diagnose/orphans.rs +66 -8
- package/crates/team-agent/src/layout/worker_window_helpers.rs +5 -7
- package/crates/team-agent/src/leader/provider_attribution.rs +7 -5
- package/crates/team-agent/src/leader/rediscover.rs +1 -11
- package/crates/team-agent/src/lifecycle/launch/plan.rs +19 -8
- package/crates/team-agent/src/lifecycle/launch.rs +135 -58
- package/crates/team-agent/src/lifecycle/lock.rs +302 -0
- package/crates/team-agent/src/lifecycle/mod.rs +1 -0
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +1 -11
- package/crates/team-agent/src/lifecycle/restart/agent.rs +169 -113
- package/crates/team-agent/src/lifecycle/restart/common.rs +108 -17
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +165 -32
- package/crates/team-agent/src/lifecycle/restart/remove.rs +16 -2
- package/crates/team-agent/src/lifecycle/restart/selection.rs +2 -1
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +6 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +3 -3
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +321 -0
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +35 -5
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +802 -0
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +648 -0
- package/crates/team-agent/src/lifecycle/tests.rs +3 -0
- package/crates/team-agent/src/lifecycle/types.rs +8 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +24 -9
- package/crates/team-agent/src/mcp_server/tools.rs +157 -68
- package/crates/team-agent/src/messaging/activity.rs +43 -18
- package/crates/team-agent/src/messaging/delivery.rs +161 -97
- package/crates/team-agent/src/messaging/helpers.rs +1 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +22 -1
- package/crates/team-agent/src/messaging/results.rs +34 -9
- package/crates/team-agent/src/messaging/scheduler.rs +52 -9
- package/crates/team-agent/src/model/spec.rs +10 -6
- package/crates/team-agent/src/provider/adapter.rs +10 -1038
- package/crates/team-agent/src/provider/adapters/claude.rs +3 -8
- package/crates/team-agent/src/provider/adapters/copilot.rs +2 -5
- package/crates/team-agent/src/provider/classify.rs +23 -48
- package/crates/team-agent/src/provider/command.rs +16 -7
- package/crates/team-agent/src/provider/faults.rs +93 -53
- package/crates/team-agent/src/provider/session/capture.rs +4 -15
- package/crates/team-agent/src/provider/session_scan/claude.rs +309 -0
- package/crates/team-agent/src/provider/session_scan/codex.rs +40 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +350 -0
- package/crates/team-agent/src/provider/session_scan/copilot.rs +202 -0
- package/crates/team-agent/src/provider/session_scan.rs +65 -27
- package/crates/team-agent/src/provider/tests/faults.rs +80 -0
- package/crates/team-agent/src/provider/types.rs +33 -1
- package/crates/team-agent/src/provider/wire.rs +171 -1
- package/crates/team-agent/src/state/identity.rs +242 -57
- package/crates/team-agent/src/state/identity_keys.rs +9 -12
- package/crates/team-agent/src/state/mod.rs +5 -1
- package/crates/team-agent/src/state/owner_gate.rs +59 -15
- package/crates/team-agent/src/state/ownership.rs +12 -11
- package/crates/team-agent/src/state/paths.rs +13 -6
- package/crates/team-agent/src/state/persist.rs +671 -128
- package/crates/team-agent/src/state/projection.rs +240 -49
- package/crates/team-agent/src/state/selector.rs +11 -5
- package/crates/team-agent/src/tmux_backend/tests.rs +51 -2
- package/crates/team-agent/src/tmux_backend.rs +42 -5
- package/crates/team-agent/src/transport/test_support.rs +55 -6
- package/package.json +4 -4
|
@@ -503,20 +503,43 @@ fn is_provider_or_mcp_workspace_command(command: &str) -> bool {
|
|
|
503
503
|
}
|
|
504
504
|
|
|
505
505
|
fn parse_workspace_arg(command: &str) -> Option<PathBuf> {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
506
|
+
for (start, _) in command.match_indices("--workspace") {
|
|
507
|
+
let boundary_before = start == 0
|
|
508
|
+
|| command
|
|
509
|
+
.get(..start)
|
|
510
|
+
.and_then(|prefix| prefix.chars().next_back())
|
|
511
|
+
.is_some_and(char::is_whitespace);
|
|
512
|
+
if !boundary_before {
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
let after = &command[start + "--workspace".len()..];
|
|
516
|
+
if let Some(value) = after.strip_prefix('=') {
|
|
517
|
+
return parse_workspace_value(value);
|
|
512
518
|
}
|
|
513
|
-
if
|
|
514
|
-
return
|
|
519
|
+
if after.chars().next().is_some_and(char::is_whitespace) {
|
|
520
|
+
return parse_workspace_value(after);
|
|
515
521
|
}
|
|
516
522
|
}
|
|
517
523
|
None
|
|
518
524
|
}
|
|
519
525
|
|
|
526
|
+
fn parse_workspace_value(value: &str) -> Option<PathBuf> {
|
|
527
|
+
let value = value.trim_start();
|
|
528
|
+
if value.is_empty() {
|
|
529
|
+
return None;
|
|
530
|
+
}
|
|
531
|
+
let value = value
|
|
532
|
+
.split_once(" --")
|
|
533
|
+
.map(|(workspace, _)| workspace)
|
|
534
|
+
.unwrap_or(value)
|
|
535
|
+
.trim_end();
|
|
536
|
+
if value.is_empty() {
|
|
537
|
+
None
|
|
538
|
+
} else {
|
|
539
|
+
Some(PathBuf::from(value))
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
520
543
|
fn classify_workspace_orphan(workspace: &Path, pid: Pid) -> Option<OrphanReason> {
|
|
521
544
|
if !workspace.is_absolute() {
|
|
522
545
|
return None;
|
|
@@ -656,3 +679,38 @@ fn reason_key(reason: &OrphanReason) -> &'static str {
|
|
|
656
679
|
OrphanReason::PidNotRunning => "pid_not_running",
|
|
657
680
|
}
|
|
658
681
|
}
|
|
682
|
+
|
|
683
|
+
#[cfg(test)]
|
|
684
|
+
mod tests {
|
|
685
|
+
use super::*;
|
|
686
|
+
|
|
687
|
+
#[test]
|
|
688
|
+
fn parse_workspace_arg_keeps_trailing_path_with_spaces() {
|
|
689
|
+
let command = "/tmp/team-agent coordinator --workspace /Volumes/projects/My Agent";
|
|
690
|
+
|
|
691
|
+
assert_eq!(
|
|
692
|
+
parse_workspace_arg(command),
|
|
693
|
+
Some(PathBuf::from("/Volumes/projects/My Agent"))
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
#[test]
|
|
698
|
+
fn parse_workspace_arg_stops_at_next_flag() {
|
|
699
|
+
let command = "/tmp/team-agent coordinator --workspace /tmp/My Agent --once";
|
|
700
|
+
|
|
701
|
+
assert_eq!(
|
|
702
|
+
parse_workspace_arg(command),
|
|
703
|
+
Some(PathBuf::from("/tmp/My Agent"))
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
#[test]
|
|
708
|
+
fn parse_workspace_arg_supports_equals_form_with_spaces() {
|
|
709
|
+
let command = "/tmp/team-agent coordinator --workspace=/tmp/My Agent --once";
|
|
710
|
+
|
|
711
|
+
assert_eq!(
|
|
712
|
+
parse_workspace_arg(command),
|
|
713
|
+
Some(PathBuf::from("/tmp/My Agent"))
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
//! 0.3.28 layout step 2 — small
|
|
1
|
+
//! 0.3.28 layout step 2 — small window grammar helper for layout-layer naming.
|
|
2
2
|
//!
|
|
3
|
-
//!
|
|
4
|
-
//!
|
|
5
|
-
//!
|
|
6
|
-
//! authoritative provider→string mapping).
|
|
3
|
+
//! Command/UI grammar, not provider identity parsing: leader window names
|
|
4
|
+
//! intentionally collapse `claude_code` to `claude` and use executable-style
|
|
5
|
+
//! labels such as `gemini`.
|
|
7
6
|
|
|
8
7
|
use crate::model::enums::Provider;
|
|
9
8
|
|
|
10
|
-
/// Window name used inside the leader session
|
|
11
|
-
/// wire identifier (e.g. `claude`, `codex`, `copilot`, `fake`). Mirrors
|
|
9
|
+
/// Window name used inside the leader session. Mirrors
|
|
12
10
|
/// Python's `leader_window_name = provider` (see
|
|
13
11
|
/// `runtime/0.2.11/src/team_agent/leader/__init__.py:114-131`).
|
|
14
12
|
pub fn provider_window_name(provider: Provider) -> &'static str {
|
|
@@ -8,7 +8,9 @@ use crate::transport::PaneInfo;
|
|
|
8
8
|
|
|
9
9
|
use super::helpers::{parse_provider, provider_wire};
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
// Command grammar, not provider identity parsing: these are process argv/current-command
|
|
12
|
+
// needles observed in tmux panes.
|
|
13
|
+
const COMMAND_GRAMMAR_PROVIDER_COMMANDS: &[(&str, Provider)] = &[
|
|
12
14
|
("claude", Provider::ClaudeCode),
|
|
13
15
|
("claude.exe", Provider::ClaudeCode),
|
|
14
16
|
("codex", Provider::Codex),
|
|
@@ -37,7 +39,7 @@ fn attribute_pane_provider_with_process(
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
pub(crate) fn attribute_command_provider(command: &str) -> Option<Provider> {
|
|
40
|
-
|
|
42
|
+
provider_from_command_text(command)
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
pub(crate) fn command_matches_provider(provider: Provider, command: &str) -> bool {
|
|
@@ -58,7 +60,7 @@ fn provider_from_env(env: &BTreeMap<String, String>) -> Option<Provider> {
|
|
|
58
60
|
fn provider_from_pid_argv(pid: u32) -> Option<Provider> {
|
|
59
61
|
process_command_line(pid)
|
|
60
62
|
.as_deref()
|
|
61
|
-
.and_then(
|
|
63
|
+
.and_then(provider_from_command_text)
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
fn provider_from_pid_env(pid: u32) -> Option<Provider> {
|
|
@@ -67,9 +69,9 @@ fn provider_from_pid_env(pid: u32) -> Option<Provider> {
|
|
|
67
69
|
.and_then(provider_from_env_text)
|
|
68
70
|
}
|
|
69
71
|
|
|
70
|
-
fn
|
|
72
|
+
fn provider_from_command_text(text: &str) -> Option<Provider> {
|
|
71
73
|
let lower = text.to_ascii_lowercase();
|
|
72
|
-
|
|
74
|
+
COMMAND_GRAMMAR_PROVIDER_COMMANDS
|
|
73
75
|
.iter()
|
|
74
76
|
.find_map(|(needle, provider)| lower.contains(needle).then_some(*provider))
|
|
75
77
|
}
|
|
@@ -7,6 +7,7 @@ use serde_json::{json, Value};
|
|
|
7
7
|
|
|
8
8
|
use crate::model::ids::{LeaderSessionUuid, OwnerEpoch};
|
|
9
9
|
use crate::provider::Provider;
|
|
10
|
+
use crate::provider::wire::provider_wire;
|
|
10
11
|
use crate::transport::{PaneId, PaneInfo, SessionName, Transport, WindowName};
|
|
11
12
|
|
|
12
13
|
use super::helpers::{get_path_str, now_ts, parse_provider, prefix, resolve_workspace_for_hash, sha1_hex_prefix};
|
|
@@ -902,17 +903,6 @@ fn uuid_prefix(uuid: Option<&LeaderSessionUuid>) -> String {
|
|
|
902
903
|
uuid.map_or_else(String::new, |value| prefix(value.as_str(), 8).to_string())
|
|
903
904
|
}
|
|
904
905
|
|
|
905
|
-
fn provider_wire(provider: Provider) -> &'static str {
|
|
906
|
-
match provider {
|
|
907
|
-
Provider::Claude => "claude",
|
|
908
|
-
Provider::ClaudeCode => "claude_code",
|
|
909
|
-
Provider::Codex => "codex",
|
|
910
|
-
Provider::Copilot => "copilot",
|
|
911
|
-
Provider::GeminiCli => "gemini_cli",
|
|
912
|
-
Provider::Fake => "fake",
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
|
|
916
906
|
fn event_provider(identity: &OwnerIdentity) -> &'static str {
|
|
917
907
|
provider_wire(identity.provider.unwrap_or(Provider::Codex))
|
|
918
908
|
}
|
|
@@ -15,9 +15,8 @@ pub fn start_plan(
|
|
|
15
15
|
plan_path.display()
|
|
16
16
|
)));
|
|
17
17
|
}
|
|
18
|
-
let text = std::fs::read_to_string(plan_path)
|
|
19
|
-
LifecycleError::InvalidPlan(format!("{}: {e}", plan_path.display()))
|
|
20
|
-
})?;
|
|
18
|
+
let text = std::fs::read_to_string(plan_path)
|
|
19
|
+
.map_err(|e| LifecycleError::InvalidPlan(format!("{}: {e}", plan_path.display())))?;
|
|
21
20
|
let plan = yaml::loads(&text).map_err(|e| LifecycleError::InvalidPlan(e.to_string()))?;
|
|
22
21
|
let plan_id = plan_id_from_plan(plan_path, &plan)?;
|
|
23
22
|
let state_path = plan_state_path(workspace, &plan_id);
|
|
@@ -66,7 +65,8 @@ pub fn handle_report_result(
|
|
|
66
65
|
return Ok(PlanProgress::NoMatch);
|
|
67
66
|
};
|
|
68
67
|
for entry in entries {
|
|
69
|
-
let entry =
|
|
68
|
+
let entry =
|
|
69
|
+
entry.map_err(|e| LifecycleError::InvalidPlan(format!("read plan dir: {e}")))?;
|
|
70
70
|
let path = entry.path();
|
|
71
71
|
if path.extension().and_then(|s| s.to_str()) != Some("json") {
|
|
72
72
|
continue;
|
|
@@ -112,10 +112,19 @@ fn plan_stages(plan: &Value) -> Result<Vec<PlanStage>, LifecycleError> {
|
|
|
112
112
|
.transpose()?;
|
|
113
113
|
stages.push(PlanStage {
|
|
114
114
|
id,
|
|
115
|
-
assignee: raw
|
|
116
|
-
|
|
115
|
+
assignee: raw
|
|
116
|
+
.get("assignee")
|
|
117
|
+
.and_then(Value::as_str)
|
|
118
|
+
.map(AgentId::new),
|
|
119
|
+
prompt: raw
|
|
120
|
+
.get("prompt")
|
|
121
|
+
.and_then(Value::as_str)
|
|
122
|
+
.map(str::to_string),
|
|
117
123
|
on_result: condition,
|
|
118
|
-
status: raw
|
|
124
|
+
status: raw
|
|
125
|
+
.get("status")
|
|
126
|
+
.and_then(Value::as_str)
|
|
127
|
+
.map(str::to_string),
|
|
119
128
|
});
|
|
120
129
|
}
|
|
121
130
|
Ok(stages)
|
|
@@ -222,6 +231,8 @@ fn json_scalar_to_string(value: &serde_json::Value) -> Option<String> {
|
|
|
222
231
|
serde_json::Value::String(s) => Some(s.clone()),
|
|
223
232
|
serde_json::Value::Bool(b) => Some(b.to_string()),
|
|
224
233
|
serde_json::Value::Number(n) => Some(n.to_string()),
|
|
225
|
-
serde_json::Value::Null | serde_json::Value::Array(_) | serde_json::Value::Object(_) =>
|
|
234
|
+
serde_json::Value::Null | serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
|
|
235
|
+
None
|
|
236
|
+
}
|
|
226
237
|
}
|
|
227
238
|
}
|
|
@@ -11,6 +11,8 @@ use crate::model::yaml::{self, Value};
|
|
|
11
11
|
use crate::state::persist::{load_runtime_state, save_runtime_state};
|
|
12
12
|
use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
|
|
13
13
|
|
|
14
|
+
use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
|
|
15
|
+
|
|
14
16
|
use super::*;
|
|
15
17
|
|
|
16
18
|
// ── lifecycle::launch —— 冷启 / quick-start / 危险审批探测 ──────────────────
|
|
@@ -124,7 +126,8 @@ pub fn launch_with_transport_in_workspace(
|
|
|
124
126
|
// Step 10 once Steps 2–9 have eliminated structural co-location.
|
|
125
127
|
if !dry_run {
|
|
126
128
|
if let Ok(state_for_check) = crate::state::persist::load_runtime_state(workspace) {
|
|
127
|
-
let violations =
|
|
129
|
+
let violations =
|
|
130
|
+
crate::layout::sessions::assert_topology_invariants(&state_for_check, &spec);
|
|
128
131
|
crate::layout::sessions::log_topology_violations(&violations);
|
|
129
132
|
}
|
|
130
133
|
}
|
|
@@ -247,9 +250,7 @@ fn spawn_agents(
|
|
|
247
250
|
// unsupported warning event when the spec asked for an effort the
|
|
248
251
|
// provider can't satisfy.
|
|
249
252
|
let agent_effort = provider_effort_for_spawn(agent, provider);
|
|
250
|
-
if let Some(event_value) =
|
|
251
|
-
provider_effort_event_if_dropped(agent, provider, agent_id_raw)
|
|
252
|
-
{
|
|
253
|
+
if let Some(event_value) = provider_effort_event_if_dropped(agent, provider, agent_id_raw) {
|
|
253
254
|
let _ = crate::event_log::EventLog::new(workspace)
|
|
254
255
|
.write("provider.effort_unsupported", event_value);
|
|
255
256
|
}
|
|
@@ -279,7 +280,11 @@ fn spawn_agents(
|
|
|
279
280
|
// 含 "model" 字面,失败信息透传给 leader。
|
|
280
281
|
if matches!(provider, Provider::Copilot) && auth_mode == AuthMode::CompatibleApi {
|
|
281
282
|
let has_model = model.is_some_and(|s| !s.is_empty())
|
|
282
|
-
|| profile_launch
|
|
283
|
+
|| profile_launch
|
|
284
|
+
.command_overrides
|
|
285
|
+
.model
|
|
286
|
+
.as_deref()
|
|
287
|
+
.is_some_and(|s| !s.is_empty())
|
|
283
288
|
|| profile_launch
|
|
284
289
|
.env_overlay
|
|
285
290
|
.get("COPILOT_MODEL")
|
|
@@ -307,9 +312,8 @@ fn spawn_agents(
|
|
|
307
312
|
.join("logs")
|
|
308
313
|
.join("copilot")
|
|
309
314
|
.join(agent_id_raw);
|
|
310
|
-
std::fs::create_dir_all(&log_dir)
|
|
311
|
-
LifecycleError::StatePersist(format!("{}: {e}", log_dir.display()))
|
|
312
|
-
})?;
|
|
315
|
+
std::fs::create_dir_all(&log_dir)
|
|
316
|
+
.map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", log_dir.display())))?;
|
|
313
317
|
plan.argv.push("--log-dir".to_string());
|
|
314
318
|
plan.argv.push(log_dir.to_string_lossy().to_string());
|
|
315
319
|
plan.argv.push("--log-level".to_string());
|
|
@@ -606,9 +610,7 @@ pub(crate) fn adaptive_placement_for_agent(
|
|
|
606
610
|
if !is_adaptive_layout_window(window) {
|
|
607
611
|
continue;
|
|
608
612
|
}
|
|
609
|
-
let pane_id = agent
|
|
610
|
-
.get("pane_id")
|
|
611
|
-
.and_then(serde_json::Value::as_str);
|
|
613
|
+
let pane_id = agent.get("pane_id").and_then(serde_json::Value::as_str);
|
|
612
614
|
let pane_live = pane_id.is_some_and(|pane| live_panes.contains(pane));
|
|
613
615
|
// E43 Fix A: window_name match — when the agent's pane is live,
|
|
614
616
|
// its live window_name MUST equal the claimed `window`; otherwise
|
|
@@ -728,7 +730,9 @@ pub(crate) fn adaptive_existing_placement_for_agent(
|
|
|
728
730
|
.unwrap_or_default()
|
|
729
731
|
.into_iter()
|
|
730
732
|
.filter(|pane| {
|
|
731
|
-
pane.window_name
|
|
733
|
+
pane.window_name
|
|
734
|
+
.as_ref()
|
|
735
|
+
.is_some_and(|name| name.as_str() == window)
|
|
732
736
|
&& agent
|
|
733
737
|
.get("pane_id")
|
|
734
738
|
.and_then(serde_json::Value::as_str)
|
|
@@ -1328,7 +1332,10 @@ fn warn_ignored_owner_team_id(workspace: &Path, team_dir: &Path, runtime_team_ke
|
|
|
1328
1332
|
let Ok(Some(ignored)) = crate::compiler::ignored_owner_team_id_from_team_md(team_dir) else {
|
|
1329
1333
|
return;
|
|
1330
1334
|
};
|
|
1331
|
-
eprintln!(
|
|
1335
|
+
eprintln!(
|
|
1336
|
+
"Warning: ignored TEAM.md {}={}",
|
|
1337
|
+
ignored.field, ignored.value
|
|
1338
|
+
);
|
|
1332
1339
|
eprintln!("Reason: owner identity is the canonical runtime team key ({runtime_team_key}), not TEAM.md front matter");
|
|
1333
1340
|
eprintln!("Action: remove {} from TEAM.md", ignored.field);
|
|
1334
1341
|
if let Err(err) = crate::event_log::EventLog::new(workspace).write(
|
|
@@ -1382,7 +1389,9 @@ fn active_leader_pane_state_across_tmux_sockets(
|
|
|
1382
1389
|
.map(|endpoint| crate::tmux_backend::TmuxBackend::for_tmux_endpoint(endpoint))
|
|
1383
1390
|
.collect::<Vec<_>>();
|
|
1384
1391
|
active_leader_pane_state_across_transports(
|
|
1385
|
-
transports
|
|
1392
|
+
transports
|
|
1393
|
+
.iter()
|
|
1394
|
+
.map(|transport| transport as &dyn Transport),
|
|
1386
1395
|
pane,
|
|
1387
1396
|
)
|
|
1388
1397
|
}
|
|
@@ -2085,12 +2094,7 @@ pub(crate) fn inherited_env_with_team_overrides(
|
|
|
2085
2094
|
// * Strips `COPILOT_DISABLE_TERMINAL_TITLE` (re-injected per-agent by
|
|
2086
2095
|
// `apply_copilot_instructions_overlay` based on the WORKER's provider).
|
|
2087
2096
|
// * Strips `TMUX` / `TMUX_PANE` (would attach worker to leader's pane).
|
|
2088
|
-
crate::layout::worker_env::worker_spawn_env(
|
|
2089
|
-
std::env::vars(),
|
|
2090
|
-
workspace,
|
|
2091
|
-
agent_id,
|
|
2092
|
-
team_id,
|
|
2093
|
-
)
|
|
2097
|
+
crate::layout::worker_env::worker_spawn_env(std::env::vars(), workspace, agent_id, team_id)
|
|
2094
2098
|
}
|
|
2095
2099
|
|
|
2096
2100
|
pub(crate) fn apply_mcp_auto_approval_env(
|
|
@@ -2112,15 +2116,30 @@ pub(crate) fn apply_mcp_auto_approval_env(
|
|
|
2112
2116
|
&& safety.inherited
|
|
2113
2117
|
{
|
|
2114
2118
|
env.insert("TEAM_AGENT_LEADER_BYPASS".to_string(), "1".to_string());
|
|
2115
|
-
env.insert(
|
|
2119
|
+
env.insert(
|
|
2120
|
+
"TEAM_AGENT_LEADER_BYPASS_SOURCE".to_string(),
|
|
2121
|
+
"leader_process".to_string(),
|
|
2122
|
+
);
|
|
2116
2123
|
if let Some(provider) = safety.provider.as_deref() {
|
|
2117
|
-
env.insert(
|
|
2124
|
+
env.insert(
|
|
2125
|
+
"TEAM_AGENT_LEADER_BYPASS_PROVIDER".to_string(),
|
|
2126
|
+
provider.to_string(),
|
|
2127
|
+
);
|
|
2118
2128
|
}
|
|
2119
2129
|
if let Some(flag) = safety.flag.as_deref() {
|
|
2120
|
-
env.insert(
|
|
2130
|
+
env.insert(
|
|
2131
|
+
"TEAM_AGENT_LEADER_BYPASS_FLAG".to_string(),
|
|
2132
|
+
flag.to_string(),
|
|
2133
|
+
);
|
|
2121
2134
|
}
|
|
2122
|
-
env.insert(
|
|
2123
|
-
|
|
2135
|
+
env.insert(
|
|
2136
|
+
"TEAM_AGENT_MCP_AUTO_APPROVE".to_string(),
|
|
2137
|
+
"team_orchestrator".to_string(),
|
|
2138
|
+
);
|
|
2139
|
+
env.insert(
|
|
2140
|
+
"TEAM_AGENT_MCP_AUTO_APPROVE_SOURCE".to_string(),
|
|
2141
|
+
"leader_bypass".to_string(),
|
|
2142
|
+
);
|
|
2124
2143
|
} else {
|
|
2125
2144
|
env.insert("TEAM_AGENT_LEADER_BYPASS".to_string(), "0".to_string());
|
|
2126
2145
|
}
|
|
@@ -2163,7 +2182,10 @@ pub(crate) fn apply_copilot_instructions_overlay(
|
|
|
2163
2182
|
// 漏关后果定级为【B5 leader 误杀同级 incident】,绝不允许 silent 跳过。
|
|
2164
2183
|
// 主案:env `COPILOT_DISABLE_TERMINAL_TITLE=1`(help-config 原文 "Can also be
|
|
2165
2184
|
// disabled via the COPILOT_DISABLE_TERMINAL_TITLE environment variable")。
|
|
2166
|
-
env.insert(
|
|
2185
|
+
env.insert(
|
|
2186
|
+
"COPILOT_DISABLE_TERMINAL_TITLE".to_string(),
|
|
2187
|
+
"1".to_string(),
|
|
2188
|
+
);
|
|
2167
2189
|
Ok(())
|
|
2168
2190
|
}
|
|
2169
2191
|
|
|
@@ -2219,7 +2241,10 @@ fn apply_copilot_mcp_residual_disables(
|
|
|
2219
2241
|
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
|
|
2220
2242
|
std::fs::write(
|
|
2221
2243
|
&residual_path,
|
|
2222
|
-
format!(
|
|
2244
|
+
format!(
|
|
2245
|
+
"copilot mcp list exit={:?} stderr={stderr}\n",
|
|
2246
|
+
out.status.code()
|
|
2247
|
+
),
|
|
2223
2248
|
)
|
|
2224
2249
|
.map_err(|e| {
|
|
2225
2250
|
LifecycleError::StatePersist(format!("{}: {e}", residual_path.display()))
|
|
@@ -2578,7 +2603,10 @@ pub(crate) fn provider_effort_for_spawn_json(
|
|
|
2578
2603
|
agent: &serde_json::Value,
|
|
2579
2604
|
provider: Provider,
|
|
2580
2605
|
) -> Option<ProviderEffort> {
|
|
2581
|
-
provider_effort_from_raw(
|
|
2606
|
+
provider_effort_from_raw(
|
|
2607
|
+
agent.get("effort").and_then(serde_json::Value::as_str),
|
|
2608
|
+
provider,
|
|
2609
|
+
)
|
|
2582
2610
|
}
|
|
2583
2611
|
|
|
2584
2612
|
pub(crate) fn provider_effort_event_if_dropped_json(
|
|
@@ -2797,14 +2825,7 @@ pub fn quick_start_in_workspace(
|
|
|
2797
2825
|
) -> Result<QuickStartReport, LifecycleError> {
|
|
2798
2826
|
let workspace = explicit_quick_start_workspace(workspace);
|
|
2799
2827
|
let transport = quick_start_tmux_backend(&workspace);
|
|
2800
|
-
quick_start_with_transport_in_workspace(
|
|
2801
|
-
&workspace,
|
|
2802
|
-
agents_dir,
|
|
2803
|
-
name,
|
|
2804
|
-
yes,
|
|
2805
|
-
team_id,
|
|
2806
|
-
&transport,
|
|
2807
|
-
)
|
|
2828
|
+
quick_start_with_transport_in_workspace(&workspace, agents_dir, name, yes, team_id, &transport)
|
|
2808
2829
|
}
|
|
2809
2830
|
|
|
2810
2831
|
pub fn quick_start_in_workspace_with_display(
|
|
@@ -2873,7 +2894,9 @@ pub(crate) fn configure_adaptive_pane_title(
|
|
|
2873
2894
|
"warning": message,
|
|
2874
2895
|
}),
|
|
2875
2896
|
) {
|
|
2876
|
-
eprintln!(
|
|
2897
|
+
eprintln!(
|
|
2898
|
+
"Warning: adaptive_layout.pane_title_failed event write failed: {event_error}"
|
|
2899
|
+
);
|
|
2877
2900
|
}
|
|
2878
2901
|
}
|
|
2879
2902
|
}
|
|
@@ -2900,9 +2923,7 @@ pub fn quick_start_with_transport(
|
|
|
2900
2923
|
transport: &dyn Transport,
|
|
2901
2924
|
) -> Result<QuickStartReport, LifecycleError> {
|
|
2902
2925
|
let workspace = team_workspace(agents_dir);
|
|
2903
|
-
quick_start_with_transport_in_workspace(
|
|
2904
|
-
&workspace, agents_dir, name, yes, team_id, transport,
|
|
2905
|
-
)
|
|
2926
|
+
quick_start_with_transport_in_workspace(&workspace, agents_dir, name, yes, team_id, transport)
|
|
2906
2927
|
}
|
|
2907
2928
|
|
|
2908
2929
|
pub fn quick_start_with_transport_in_workspace(
|
|
@@ -3090,7 +3111,10 @@ pub fn quick_start_with_transport_in_workspace_with_display(
|
|
|
3090
3111
|
let attach_windows = load_runtime_state(&workspace)
|
|
3091
3112
|
.ok()
|
|
3092
3113
|
.map(|state| {
|
|
3093
|
-
attach_window_names_with_managed_leader(
|
|
3114
|
+
attach_window_names_with_managed_leader(
|
|
3115
|
+
&state,
|
|
3116
|
+
started_attach_window_names(&launch.started),
|
|
3117
|
+
)
|
|
3094
3118
|
})
|
|
3095
3119
|
.unwrap_or_else(|| started_attach_window_names(&launch.started));
|
|
3096
3120
|
let attach_commands = attach_commands_for_runtime_windows(
|
|
@@ -3110,10 +3134,13 @@ pub fn quick_start_with_transport_in_workspace_with_display(
|
|
|
3110
3134
|
next_actions.push(
|
|
3111
3135
|
"quick-start initialized this team; for all subsequent starts use \
|
|
3112
3136
|
`team-agent restart` to resume context (quick-start refuses on \
|
|
3113
|
-
already-initialised teams)"
|
|
3137
|
+
already-initialised teams)"
|
|
3138
|
+
.to_string(),
|
|
3114
3139
|
);
|
|
3115
3140
|
if crate::tmux_backend::socket_probe_missing_for_workspace(&workspace) {
|
|
3116
|
-
next_actions.push(crate::tmux_backend::socket_missing_hint_for_workspace(
|
|
3141
|
+
next_actions.push(crate::tmux_backend::socket_missing_hint_for_workspace(
|
|
3142
|
+
&workspace,
|
|
3143
|
+
));
|
|
3117
3144
|
}
|
|
3118
3145
|
next_actions.extend(attach_commands.iter().cloned());
|
|
3119
3146
|
let display_backend = state
|
|
@@ -3137,7 +3164,11 @@ pub fn quick_start_with_transport_in_workspace_with_display(
|
|
|
3137
3164
|
/// the persisted endpoint synchronized with the transport they actually used,
|
|
3138
3165
|
/// closing the silent socket-drift gap** (single state-save path; no parallel
|
|
3139
3166
|
/// "annotate after spawn" race with coordinator).
|
|
3140
|
-
pub(crate) fn annotate_runtime_tmux_endpoint(
|
|
3167
|
+
pub(crate) fn annotate_runtime_tmux_endpoint(
|
|
3168
|
+
state: &mut serde_json::Value,
|
|
3169
|
+
transport: &dyn Transport,
|
|
3170
|
+
workspace: &Path,
|
|
3171
|
+
) {
|
|
3141
3172
|
let Some(endpoint) = transport.tmux_endpoint() else {
|
|
3142
3173
|
return;
|
|
3143
3174
|
};
|
|
@@ -3153,7 +3184,10 @@ pub(crate) fn annotate_runtime_tmux_endpoint(state: &mut serde_json::Value, tran
|
|
|
3153
3184
|
.unwrap_or_else(|| endpoint.clone())
|
|
3154
3185
|
};
|
|
3155
3186
|
if let Some(obj) = state.as_object_mut() {
|
|
3156
|
-
obj.insert(
|
|
3187
|
+
obj.insert(
|
|
3188
|
+
"tmux_endpoint".to_string(),
|
|
3189
|
+
serde_json::json!(endpoint_for_state),
|
|
3190
|
+
);
|
|
3157
3191
|
obj.insert(
|
|
3158
3192
|
"tmux_socket".to_string(),
|
|
3159
3193
|
obj.get("tmux_endpoint")
|
|
@@ -3430,6 +3464,7 @@ fn detect_dangerous_approval_in_argv(argv_tokens: &[String]) -> Option<Dangerous
|
|
|
3430
3464
|
}
|
|
3431
3465
|
|
|
3432
3466
|
fn dangerous_leader_flags() -> &'static [(&'static str, &'static str)] {
|
|
3467
|
+
// Process argv command grammar, not persisted provider identity parsing.
|
|
3433
3468
|
&[
|
|
3434
3469
|
("claude", "--dangerously-skip-permissions"),
|
|
3435
3470
|
("claude", "--dangerously-skip-permission"),
|
|
@@ -3445,6 +3480,7 @@ fn dangerous_leader_flags() -> &'static [(&'static str, &'static str)] {
|
|
|
3445
3480
|
}
|
|
3446
3481
|
|
|
3447
3482
|
fn binary_matches_provider(provider: &str, binary: Option<&str>) -> bool {
|
|
3483
|
+
// Binary-name command grammar, not provider identity parsing.
|
|
3448
3484
|
match (provider, binary) {
|
|
3449
3485
|
("codex", Some("codex")) => true,
|
|
3450
3486
|
("claude", Some("claude" | "claude-code" | "claude_code")) => true,
|
|
@@ -3607,10 +3643,11 @@ pub fn add_agent(
|
|
|
3607
3643
|
// team uses. Cold workspaces / first-agent paths safely fall back to
|
|
3608
3644
|
// `TmuxBackend::for_workspace(team_workspace)` inside the resolver.
|
|
3609
3645
|
let team_ws = team_workspace(workspace);
|
|
3610
|
-
let transport =
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3646
|
+
let transport =
|
|
3647
|
+
crate::lifecycle::restart::lifecycle_worker_tmux_backend_for_selected_state(
|
|
3648
|
+
&team_ws, team,
|
|
3649
|
+
)
|
|
3650
|
+
.unwrap_or_else(|_| crate::tmux_backend::TmuxBackend::for_workspace(&team_ws));
|
|
3614
3651
|
return add_agent_with_transport(
|
|
3615
3652
|
workspace,
|
|
3616
3653
|
agent_id,
|
|
@@ -3622,6 +3659,12 @@ pub fn add_agent(
|
|
|
3622
3659
|
}
|
|
3623
3660
|
Err(error) => return Err(LifecycleError::TeamSelect(error.to_string())),
|
|
3624
3661
|
};
|
|
3662
|
+
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
3663
|
+
workspace: &selected.run_workspace,
|
|
3664
|
+
operation: "add-agent",
|
|
3665
|
+
team: Some(selected.team_key.as_str()),
|
|
3666
|
+
agent_id: Some(agent_id),
|
|
3667
|
+
})?;
|
|
3625
3668
|
// E5 §3:compile_team 要角色定义目录(team_dir),不是 spec 落点(spec_workspace=runtime)。
|
|
3626
3669
|
let team_dir = selected.team_dir;
|
|
3627
3670
|
// **0.3.24 add-agent socket drift fix**: route to the live team's persisted
|
|
@@ -3658,6 +3701,12 @@ pub fn add_agent_with_transport(
|
|
|
3658
3701
|
) -> Result<AddAgentReport, LifecycleError> {
|
|
3659
3702
|
let run_workspace = crate::model::paths::canonical_run_workspace(workspace)
|
|
3660
3703
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
3704
|
+
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
3705
|
+
workspace: &run_workspace,
|
|
3706
|
+
operation: "add-agent",
|
|
3707
|
+
team,
|
|
3708
|
+
agent_id: Some(agent_id),
|
|
3709
|
+
})?;
|
|
3661
3710
|
add_agent_with_transport_at_paths(
|
|
3662
3711
|
&run_workspace,
|
|
3663
3712
|
workspace,
|
|
@@ -3950,7 +3999,11 @@ fn inject_agent_into_spec(
|
|
|
3950
3999
|
agents.push(agent);
|
|
3951
4000
|
}
|
|
3952
4001
|
}
|
|
3953
|
-
_ =>
|
|
4002
|
+
_ => {
|
|
4003
|
+
return Err(LifecycleError::Compile(
|
|
4004
|
+
"spec.agents missing or not a list".to_string(),
|
|
4005
|
+
))
|
|
4006
|
+
}
|
|
3954
4007
|
}
|
|
3955
4008
|
// routing.rules 追加 route-<id>(与 compile_team 同形)。
|
|
3956
4009
|
if let Some((_, Value::Map(routing))) = pairs.iter_mut().find(|(k, _)| k == "routing") {
|
|
@@ -4062,12 +4115,27 @@ pub fn fork_agent_with_transport(
|
|
|
4062
4115
|
crate::state::selector::SelectorMode::RequireSpec,
|
|
4063
4116
|
)
|
|
4064
4117
|
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
|
|
4118
|
+
let lock_workspace = selected.run_workspace.clone();
|
|
4119
|
+
let lock_team_key = selected.team_key.clone();
|
|
4120
|
+
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
4121
|
+
workspace: &lock_workspace,
|
|
4122
|
+
operation: "fork-agent",
|
|
4123
|
+
team: Some(lock_team_key.as_str()),
|
|
4124
|
+
agent_id: Some(as_agent_id),
|
|
4125
|
+
})?;
|
|
4126
|
+
let selected = crate::state::selector::resolve_active_team(
|
|
4127
|
+
&lock_workspace,
|
|
4128
|
+
Some(lock_team_key.as_str()),
|
|
4129
|
+
crate::state::selector::SelectorMode::RequireSpec,
|
|
4130
|
+
)
|
|
4131
|
+
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
|
|
4065
4132
|
// E5 §3:team_dir(角色定义+profiles)恒用户目录。spec 读用 selector 解析的 spec_path
|
|
4066
4133
|
// (读序 B:runtime 优先、legacy 回落),写恒走 runtime_spec_path(canonical 落点)。
|
|
4067
4134
|
let fork_team_dir = selected.team_dir.clone();
|
|
4068
|
-
let read_spec_path = selected
|
|
4069
|
-
|
|
4070
|
-
|
|
4135
|
+
let read_spec_path = selected
|
|
4136
|
+
.spec_path
|
|
4137
|
+
.clone()
|
|
4138
|
+
.ok_or_else(|| LifecycleError::TeamSelect("active team spec not found".to_string()))?;
|
|
4071
4139
|
let workspace = selected.run_workspace;
|
|
4072
4140
|
let state = selected.state;
|
|
4073
4141
|
ensure_owner_allowed_for_state(&state, Some(source_agent_id))?;
|
|
@@ -4141,8 +4209,8 @@ pub fn fork_agent_with_transport(
|
|
|
4141
4209
|
}
|
|
4142
4210
|
let new_spec = append_forked_agent(&spec, source_agent, source_agent_id, as_agent_id, label)?;
|
|
4143
4211
|
// validate 用角色定义目录的 team_workspace(校验 working_directory),非 spec 落点。
|
|
4144
|
-
let validate_ws =
|
|
4145
|
-
.unwrap_or_else(|_| workspace.clone());
|
|
4212
|
+
let validate_ws =
|
|
4213
|
+
crate::model::paths::team_workspace(&fork_team_dir).unwrap_or_else(|_| workspace.clone());
|
|
4146
4214
|
crate::model::spec::validate_spec(&new_spec, &validate_ws)
|
|
4147
4215
|
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
4148
4216
|
write_spec_atomic(&spec_path, &new_spec)?;
|
|
@@ -4322,7 +4390,11 @@ pub fn fork_agent_with_transport(
|
|
|
4322
4390
|
);
|
|
4323
4391
|
return Err(e);
|
|
4324
4392
|
}
|
|
4325
|
-
if let Err(e) =
|
|
4393
|
+
if let Err(e) = crate::state::persist::save_runtime_state_with_lifecycle_topology_authority(
|
|
4394
|
+
&workspace,
|
|
4395
|
+
&next_state,
|
|
4396
|
+
&[as_agent_id.as_str()],
|
|
4397
|
+
) {
|
|
4326
4398
|
rollback_fork_after_spawn(
|
|
4327
4399
|
&workspace,
|
|
4328
4400
|
&spec_path,
|
|
@@ -4400,7 +4472,11 @@ fn rollback_fork_after_spawn(
|
|
|
4400
4472
|
window: window.clone(),
|
|
4401
4473
|
});
|
|
4402
4474
|
let _ = std::fs::write(spec_path, spec_text.as_bytes());
|
|
4403
|
-
let _ =
|
|
4475
|
+
let _ = crate::state::persist::save_runtime_state_with_deleted_agents(
|
|
4476
|
+
workspace,
|
|
4477
|
+
old_state,
|
|
4478
|
+
&[agent_id.as_str()],
|
|
4479
|
+
);
|
|
4404
4480
|
cleanup_fork_mcp_artifacts(workspace, agent_id, mcp_config_path, profile_launch);
|
|
4405
4481
|
}
|
|
4406
4482
|
|
|
@@ -4948,7 +5024,8 @@ pub(crate) fn override_spec_workspace(spec: &mut Value, workspace: &Path) {
|
|
|
4948
5024
|
if let Some((_, Value::List(agents))) = root.iter_mut().find(|(k, _)| k == "agents") {
|
|
4949
5025
|
for agent in agents {
|
|
4950
5026
|
if let Value::Map(fields) = agent {
|
|
4951
|
-
if let Some((_, value)) = fields.iter_mut().find(|(k, _)| k == "working_directory")
|
|
5027
|
+
if let Some((_, value)) = fields.iter_mut().find(|(k, _)| k == "working_directory")
|
|
5028
|
+
{
|
|
4952
5029
|
*value = Value::Str(workspace_s.clone());
|
|
4953
5030
|
}
|
|
4954
5031
|
}
|