@team-agent/installer 0.3.3 → 0.3.5
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 +8 -0
- package/crates/team-agent/src/cli/diagnose.rs +52 -11
- package/crates/team-agent/src/cli/emit.rs +3 -2
- package/crates/team-agent/src/cli/mod.rs +225 -80
- package/crates/team-agent/src/cli/send.rs +1 -0
- package/crates/team-agent/src/cli/status_port.rs +135 -7
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +8 -1
- package/crates/team-agent/src/cli/tests/mod.rs +1 -0
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +39 -0
- package/crates/team-agent/src/cli/types.rs +5 -1
- package/crates/team-agent/src/compiler/tests.rs +2 -2
- package/crates/team-agent/src/compiler.rs +1 -1
- package/crates/team-agent/src/coordinator/backoff.rs +57 -9
- package/crates/team-agent/src/coordinator/health.rs +65 -2
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +28 -16
- package/crates/team-agent/src/coordinator/tests/a0_lostupdate.rs +87 -0
- package/crates/team-agent/src/coordinator/tests/mod.rs +1 -0
- package/crates/team-agent/src/coordinator/tests/watch.rs +4 -2
- package/crates/team-agent/src/coordinator/tick.rs +195 -43
- package/crates/team-agent/src/leader/helpers.rs +2 -0
- package/crates/team-agent/src/leader/rediscover.rs +1 -0
- package/crates/team-agent/src/leader/start.rs +9 -1
- package/crates/team-agent/src/leader/takeover.rs +18 -1
- package/crates/team-agent/src/lifecycle/display.rs +3 -3
- package/crates/team-agent/src/lifecycle/launch.rs +772 -285
- package/crates/team-agent/src/lifecycle/mod.rs +1 -0
- package/crates/team-agent/src/lifecycle/profile_launch.rs +110 -4
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +4 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +16 -5
- package/crates/team-agent/src/lifecycle/restart/common.rs +35 -25
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +31 -25
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/core.rs +5 -5
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +4 -4
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +5 -3
- package/crates/team-agent/src/lifecycle/types.rs +4 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +361 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +2 -1
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +14 -1
- package/crates/team-agent/src/mcp_server/tests/send.rs +15 -1
- package/crates/team-agent/src/mcp_server/tools.rs +65 -9
- package/crates/team-agent/src/mcp_server/wire.rs +2 -1
- package/crates/team-agent/src/message_store.rs +80 -0
- package/crates/team-agent/src/messaging/results.rs +76 -5
- package/crates/team-agent/src/messaging/send.rs +3 -1
- package/crates/team-agent/src/messaging/types.rs +15 -1
- package/crates/team-agent/src/messaging/watchers.rs +68 -30
- package/crates/team-agent/src/model/enums.rs +7 -1
- package/crates/team-agent/src/model/permissions.rs +7 -0
- package/crates/team-agent/src/model/spec.rs +3 -1
- package/crates/team-agent/src/provider/adapter.rs +472 -7
- package/crates/team-agent/src/provider/classify.rs +6 -2
- package/crates/team-agent/src/provider/faults.rs +3 -2
- package/crates/team-agent/src/provider/startup_prompt.rs +25 -7
- package/crates/team-agent/src/provider/types.rs +11 -0
- package/crates/team-agent/src/session_capture.rs +1 -0
- package/crates/team-agent/src/state/persist.rs +95 -19
- package/crates/team-agent/src/tmux_backend/tests.rs +8 -7
- package/crates/team-agent/src/tmux_backend.rs +134 -6
- package/crates/team-agent/src/transport.rs +32 -0
- package/package.json +4 -4
|
@@ -385,6 +385,31 @@ fn provider_env_exports(
|
|
|
385
385
|
exports.insert("GEMINI_API_KEY".to_string(), value.to_string());
|
|
386
386
|
}
|
|
387
387
|
}
|
|
388
|
+
// C-A-4 cr verdict v2 — copilot BYOK(== compatible_api 档,help-providers 原文
|
|
389
|
+
// "GitHub authentication is not required" when COPILOT_PROVIDER_BASE_URL set):
|
|
390
|
+
// profile 值经 env_overlay 落 COPILOT_PROVIDER_BASE_URL/TYPE/API_KEY/WIRE_API
|
|
391
|
+
// + COPILOT_MODEL。BYOK 必须含 MODEL("A model is required for BYOK")—
|
|
392
|
+
// 这一硬性校验在 launch 路径处理(profile compile 时无法判别 model 来源)。
|
|
393
|
+
// Subscription/OfficialApi 不导出 COPILOT_PROVIDER_*(避免误改 auth 通道)。
|
|
394
|
+
Provider::Copilot => {
|
|
395
|
+
if auth_mode == AuthMode::CompatibleApi {
|
|
396
|
+
if let Some(value) = value_or_alternate(values, "COPILOT_PROVIDER_BASE_URL", "BASE_URL") {
|
|
397
|
+
exports.insert("COPILOT_PROVIDER_BASE_URL".to_string(), value.to_string());
|
|
398
|
+
}
|
|
399
|
+
if let Some(value) = value_or_alternate(values, "COPILOT_PROVIDER_TYPE", "PROVIDER_TYPE") {
|
|
400
|
+
exports.insert("COPILOT_PROVIDER_TYPE".to_string(), value.to_string());
|
|
401
|
+
}
|
|
402
|
+
if let Some(value) = value_or_alternate(values, "COPILOT_PROVIDER_API_KEY", "API_KEY") {
|
|
403
|
+
exports.insert("COPILOT_PROVIDER_API_KEY".to_string(), value.to_string());
|
|
404
|
+
}
|
|
405
|
+
if let Some(value) = value_or_alternate(values, "COPILOT_PROVIDER_WIRE_API", "WIRE_API") {
|
|
406
|
+
exports.insert("COPILOT_PROVIDER_WIRE_API".to_string(), value.to_string());
|
|
407
|
+
}
|
|
408
|
+
if let Some(value) = value_or_alternate(values, "COPILOT_MODEL", "MODEL") {
|
|
409
|
+
exports.insert("COPILOT_MODEL".to_string(), value.to_string());
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
388
413
|
Provider::Fake => {}
|
|
389
414
|
}
|
|
390
415
|
exports
|
|
@@ -412,6 +437,9 @@ fn provider_env_unsets(provider: Provider, auth_mode: AuthMode) -> BTreeSet<Stri
|
|
|
412
437
|
unsets.insert("GEMINI_API_KEY".to_string());
|
|
413
438
|
}
|
|
414
439
|
}
|
|
440
|
+
// C-7-1 cr verdict: 一期 subscription-only(已登录态),exports/unsets 空;
|
|
441
|
+
// BYOK(COPILOT_PROVIDER_*)二期立项时单独 cr verdict 再开。
|
|
442
|
+
Provider::Copilot => {}
|
|
415
443
|
Provider::Fake => {}
|
|
416
444
|
}
|
|
417
445
|
unsets
|
|
@@ -429,7 +457,48 @@ fn provider_command_overrides(
|
|
|
429
457
|
_ if agent.model.is_some() => agent.model.clone(),
|
|
430
458
|
_ => profile_model(values).map(str::to_string),
|
|
431
459
|
};
|
|
432
|
-
|
|
460
|
+
let mut codex_profile = None;
|
|
461
|
+
let mut codex_config = Vec::new();
|
|
462
|
+
// Python provider_env.py:62-79 (_provider_command_overrides codex branch).
|
|
463
|
+
match agent.provider {
|
|
464
|
+
Provider::Codex => {
|
|
465
|
+
codex_profile = value_or_alternate(values, "CODEX_PROFILE", "NATIVE_PROFILE")
|
|
466
|
+
.map(str::to_string);
|
|
467
|
+
let model_provider = values.get("MODEL_PROVIDER").filter(|v| !v.is_empty());
|
|
468
|
+
let base_url = values.get("BASE_URL").filter(|v| !v.is_empty());
|
|
469
|
+
if agent.auth_mode == AuthMode::CompatibleApi {
|
|
470
|
+
if let (Some(model_provider), Some(base_url)) = (model_provider, base_url) {
|
|
471
|
+
if safe_codex_provider_id(model_provider) {
|
|
472
|
+
codex_config.push(format!("model_provider=\"{model_provider}\""));
|
|
473
|
+
let prefix = format!("model_providers.{model_provider}");
|
|
474
|
+
codex_config.push(format!("{prefix}.base_url=\"{base_url}\""));
|
|
475
|
+
codex_config
|
|
476
|
+
.push(format!("{prefix}.env_key=\"TEAM_AGENT_PROVIDER_API_KEY\""));
|
|
477
|
+
if let Some(wire_api) = values.get("WIRE_API").filter(|v| !v.is_empty()) {
|
|
478
|
+
codex_config.push(format!("{prefix}.wire_api=\"{wire_api}\""));
|
|
479
|
+
}
|
|
480
|
+
if let Some(name) = values.get("PROVIDER_NAME").filter(|v| !v.is_empty()) {
|
|
481
|
+
codex_config.push(format!("{prefix}.name=\"{name}\""));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
_ => {}
|
|
488
|
+
}
|
|
489
|
+
ProviderCommandOverrides {
|
|
490
|
+
model,
|
|
491
|
+
codex_profile,
|
|
492
|
+
codex_config,
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/// Python helpers.py:33-34 `_safe_codex_provider_id`: `[A-Za-z0-9_-]+`.
|
|
497
|
+
fn safe_codex_provider_id(value: &str) -> bool {
|
|
498
|
+
!value.is_empty()
|
|
499
|
+
&& value
|
|
500
|
+
.chars()
|
|
501
|
+
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
|
|
433
502
|
}
|
|
434
503
|
|
|
435
504
|
fn write_runtime_env_file(
|
|
@@ -636,15 +705,48 @@ fn mcp_server_names(mcp_servers: &serde_json::Value) -> Vec<String> {
|
|
|
636
705
|
.unwrap_or_default()
|
|
637
706
|
}
|
|
638
707
|
|
|
708
|
+
/// swallow batch 4 C1-C4 (MUST-NOT-9/13): a CORRUPT provider-config JSON fails
|
|
709
|
+
/// EXPLICITLY — the old `unwrap_or_default` turned a parse failure into an empty map
|
|
710
|
+
/// that the subsequent write flattened back over the user's file (silent destructive
|
|
711
|
+
/// rewrite). A missing file stays Ok(empty) — only an existing-but-unparseable file
|
|
712
|
+
/// errors, naming the path + parse detail + next action; the file is never touched.
|
|
639
713
|
fn read_json_object(
|
|
640
714
|
path: &Path,
|
|
641
715
|
) -> Result<serde_json::Map<String, serde_json::Value>, LifecycleError> {
|
|
642
|
-
let
|
|
643
|
-
|
|
716
|
+
let text = match std::fs::read_to_string(path) {
|
|
717
|
+
Ok(text) => text,
|
|
718
|
+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
|
|
719
|
+
return Ok(serde_json::Map::new());
|
|
720
|
+
}
|
|
721
|
+
Err(error) => {
|
|
722
|
+
return Err(LifecycleError::RequirementUnmet(format!(
|
|
723
|
+
"profile_invalid_json: {} could not be read: {error}; fix or remove the file (or restore a backup) and relaunch",
|
|
724
|
+
path.display()
|
|
725
|
+
)));
|
|
726
|
+
}
|
|
644
727
|
};
|
|
645
728
|
match serde_json::from_str::<serde_json::Value>(&text) {
|
|
646
729
|
Ok(serde_json::Value::Object(map)) => Ok(map),
|
|
647
|
-
Ok(
|
|
730
|
+
Ok(other) => Err(LifecycleError::RequirementUnmet(format!(
|
|
731
|
+
"profile_invalid_json: {} is not a JSON object (found {}); fix or remove the file (or restore a backup) and relaunch",
|
|
732
|
+
path.display(),
|
|
733
|
+
json_kind(&other)
|
|
734
|
+
))),
|
|
735
|
+
Err(error) => Err(LifecycleError::RequirementUnmet(format!(
|
|
736
|
+
"profile_invalid_json: {} could not be parsed: {error}; fix or remove the file (or restore a backup) and relaunch",
|
|
737
|
+
path.display()
|
|
738
|
+
))),
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
fn json_kind(value: &serde_json::Value) -> &'static str {
|
|
743
|
+
match value {
|
|
744
|
+
serde_json::Value::Null => "null",
|
|
745
|
+
serde_json::Value::Bool(_) => "a boolean",
|
|
746
|
+
serde_json::Value::Number(_) => "a number",
|
|
747
|
+
serde_json::Value::String(_) => "a string",
|
|
748
|
+
serde_json::Value::Array(_) => "an array",
|
|
749
|
+
serde_json::Value::Object(_) => "an object",
|
|
648
750
|
}
|
|
649
751
|
}
|
|
650
752
|
|
|
@@ -732,6 +834,9 @@ fn required_profile_keys(provider: Provider, auth_mode: AuthMode) -> &'static [&
|
|
|
732
834
|
AuthMode::Subscription => &[],
|
|
733
835
|
},
|
|
734
836
|
Provider::GeminiCli => &["API_KEY"],
|
|
837
|
+
// C-7-1 cr verdict: copilot 一期 subscription-only,无 BYOK required key 集合;
|
|
838
|
+
// 二期 BYOK 立项时填(向后兼容字段保留)。
|
|
839
|
+
Provider::Copilot => &[],
|
|
735
840
|
Provider::Fake => &[],
|
|
736
841
|
}
|
|
737
842
|
}
|
|
@@ -794,6 +899,7 @@ pub(crate) fn parse_provider(raw: &str) -> Option<Provider> {
|
|
|
794
899
|
"claude" => Some(Provider::Claude),
|
|
795
900
|
"claude_code" => Some(Provider::ClaudeCode),
|
|
796
901
|
"codex" => Some(Provider::Codex),
|
|
902
|
+
"copilot" => Some(Provider::Copilot),
|
|
797
903
|
"gemini_cli" => Some(Provider::GeminiCli),
|
|
798
904
|
"fake" => Some(Provider::Fake),
|
|
799
905
|
_ => None,
|
|
@@ -184,7 +184,9 @@ impl SmokeTarget {
|
|
|
184
184
|
kind: SmokeKind::OpenAi,
|
|
185
185
|
})
|
|
186
186
|
}
|
|
187
|
-
|
|
187
|
+
// C-7-1 cr verdict: copilot 一期 subscription-only,无 BYOK HTTP smoke
|
|
188
|
+
// 入口;同 GeminiCli/Fake 走 unsupported_provider_smoke_skipped。
|
|
189
|
+
Provider::Copilot | Provider::GeminiCli | Provider::Fake => Err("unsupported_provider_smoke_skipped"),
|
|
188
190
|
}
|
|
189
191
|
}
|
|
190
192
|
|
|
@@ -508,6 +510,7 @@ fn provider_wire(provider: Provider) -> &'static str {
|
|
|
508
510
|
Provider::Claude => "claude",
|
|
509
511
|
Provider::ClaudeCode => "claude_code",
|
|
510
512
|
Provider::Codex => "codex",
|
|
513
|
+
Provider::Copilot => "copilot",
|
|
511
514
|
Provider::GeminiCli => "gemini_cli",
|
|
512
515
|
Provider::Fake => "fake",
|
|
513
516
|
}
|
|
@@ -451,10 +451,21 @@ fn write_start_agent_start_event(
|
|
|
451
451
|
let adapter = crate::provider::get_adapter(provider);
|
|
452
452
|
// Contract C / F6.4: event log must record the same context-aware argv that the
|
|
453
453
|
// actual spawn used — so the role/tools/MCP context appears in `start_agent.agent_start`.
|
|
454
|
-
let role = agent.get("role").and_then(|v| v.as_str());
|
|
455
454
|
let safety = crate::lifecycle::launch::effective_runtime_config_for_worker_spawn()?;
|
|
456
|
-
let
|
|
457
|
-
|
|
455
|
+
let command_agent =
|
|
456
|
+
crate::lifecycle::worker_command_context::WorkerCommandAgent::from_json(
|
|
457
|
+
agent,
|
|
458
|
+
Some(agent_id.as_str()),
|
|
459
|
+
provider,
|
|
460
|
+
);
|
|
461
|
+
let system_prompt =
|
|
462
|
+
crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
|
|
463
|
+
let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
|
|
464
|
+
&command_agent,
|
|
465
|
+
provider,
|
|
466
|
+
&safety,
|
|
467
|
+
)?;
|
|
468
|
+
let resolved_tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
|
|
458
469
|
let mcp_config = adapter
|
|
459
470
|
.mcp_config(auth_mode)
|
|
460
471
|
.map_err(|e| LifecycleError::Provider(e.to_string()))?;
|
|
@@ -484,9 +495,9 @@ fn write_start_agent_start_event(
|
|
|
484
495
|
let context = crate::provider::ProviderCommandContext {
|
|
485
496
|
auth_mode,
|
|
486
497
|
mcp_config: Some(&mcp_config),
|
|
487
|
-
system_prompt:
|
|
498
|
+
system_prompt: Some(system_prompt.as_str()),
|
|
488
499
|
model: command_model,
|
|
489
|
-
tools: &
|
|
500
|
+
tools: &resolved_tool_refs,
|
|
490
501
|
profile_launch: Some(&profile_launch),
|
|
491
502
|
};
|
|
492
503
|
let mut plan = match session_id {
|
|
@@ -29,7 +29,6 @@ pub(super) fn spawn_agent_window(
|
|
|
29
29
|
// Contract C / F6.4: thread compiled role/tools/MCP context through restart as well —
|
|
30
30
|
// a restarted worker must come back up with the SAME callable MCP capability + role
|
|
31
31
|
// prompt as a fresh launch, else `report_result` becomes unreachable after every restart.
|
|
32
|
-
let role = agent.get("role").and_then(|v| v.as_str());
|
|
33
32
|
let detected_safety;
|
|
34
33
|
let safety = if let Some(safety) = safety {
|
|
35
34
|
safety
|
|
@@ -37,8 +36,20 @@ pub(super) fn spawn_agent_window(
|
|
|
37
36
|
detected_safety = crate::lifecycle::launch::effective_runtime_config_for_worker_spawn()?;
|
|
38
37
|
&detected_safety
|
|
39
38
|
};
|
|
40
|
-
let
|
|
41
|
-
|
|
39
|
+
let command_agent =
|
|
40
|
+
crate::lifecycle::worker_command_context::WorkerCommandAgent::from_json(
|
|
41
|
+
agent,
|
|
42
|
+
Some(agent_id.as_str()),
|
|
43
|
+
provider,
|
|
44
|
+
);
|
|
45
|
+
let system_prompt =
|
|
46
|
+
crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
|
|
47
|
+
let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
|
|
48
|
+
&command_agent,
|
|
49
|
+
provider,
|
|
50
|
+
safety,
|
|
51
|
+
)?;
|
|
52
|
+
let resolved_tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
|
|
42
53
|
// owner_team_id resolution: prefer the runtime-state row's `owner_team_id` (set by
|
|
43
54
|
// launch/restart); fall back to the active team key for paths that don't write the
|
|
44
55
|
// row first (e.g. add-agent calls spawn before upserting team metadata).
|
|
@@ -78,9 +89,9 @@ pub(super) fn spawn_agent_window(
|
|
|
78
89
|
let context = crate::provider::ProviderCommandContext {
|
|
79
90
|
auth_mode,
|
|
80
91
|
mcp_config: Some(&mcp_config),
|
|
81
|
-
system_prompt:
|
|
92
|
+
system_prompt: Some(system_prompt.as_str()),
|
|
82
93
|
model: command_model,
|
|
83
|
-
tools: &
|
|
94
|
+
tools: &resolved_tool_refs,
|
|
84
95
|
profile_launch: Some(&profile_launch),
|
|
85
96
|
};
|
|
86
97
|
let mut plan = match resume_session_id {
|
|
@@ -120,10 +131,25 @@ pub(super) fn spawn_agent_window(
|
|
|
120
131
|
.map(Path::new)
|
|
121
132
|
})
|
|
122
133
|
.unwrap_or(workspace);
|
|
134
|
+
let env_unset: Vec<String> = profile_launch.env_unset.iter().cloned().collect();
|
|
123
135
|
let result = if into_existing_session {
|
|
124
|
-
transport.
|
|
136
|
+
transport.spawn_into_with_env_unset(
|
|
137
|
+
session_name,
|
|
138
|
+
&window,
|
|
139
|
+
&plan.argv,
|
|
140
|
+
spawn_cwd,
|
|
141
|
+
&env,
|
|
142
|
+
&env_unset,
|
|
143
|
+
)
|
|
125
144
|
} else {
|
|
126
|
-
transport.
|
|
145
|
+
transport.spawn_first_with_env_unset(
|
|
146
|
+
session_name,
|
|
147
|
+
&window,
|
|
148
|
+
&plan.argv,
|
|
149
|
+
spawn_cwd,
|
|
150
|
+
&env,
|
|
151
|
+
&env_unset,
|
|
152
|
+
)
|
|
127
153
|
};
|
|
128
154
|
let spawn = result.map_err(|e| LifecycleError::Transport(e.to_string()))?;
|
|
129
155
|
let _ = adapter.handle_startup_prompts(
|
|
@@ -319,24 +345,6 @@ pub(crate) fn restart_required_missing_session_agent_ids(state: &serde_json::Val
|
|
|
319
345
|
missing.sort();
|
|
320
346
|
missing
|
|
321
347
|
}
|
|
322
|
-
/// Tools list off an agent's runtime state entry (`tools: [...]`). Restart paths
|
|
323
|
-
/// don't have the full spec object, only the runtime state — so they read tools from
|
|
324
|
-
/// the state row, falling back to an empty list. Contract C requires the worker
|
|
325
|
-
/// command be built with the tool list, even on restart.
|
|
326
|
-
pub(super) fn agent_tool_strings(agent: &serde_json::Value) -> Vec<String> {
|
|
327
|
-
agent
|
|
328
|
-
.get("tools")
|
|
329
|
-
.and_then(|v| v.as_array())
|
|
330
|
-
.map(|items| {
|
|
331
|
-
items
|
|
332
|
-
.iter()
|
|
333
|
-
.filter_map(|v| v.as_str())
|
|
334
|
-
.map(str::to_string)
|
|
335
|
-
.collect()
|
|
336
|
-
})
|
|
337
|
-
.unwrap_or_default()
|
|
338
|
-
}
|
|
339
|
-
|
|
340
348
|
pub(super) fn agent_window(agent: &serde_json::Value, agent_id: &AgentId) -> String {
|
|
341
349
|
agent
|
|
342
350
|
.get("window")
|
|
@@ -351,6 +359,7 @@ pub(super) fn parse_provider(raw: &str) -> Option<Provider> {
|
|
|
351
359
|
"claude" => Some(Provider::Claude),
|
|
352
360
|
"claude_code" => Some(Provider::ClaudeCode),
|
|
353
361
|
"codex" => Some(Provider::Codex),
|
|
362
|
+
"copilot" => Some(Provider::Copilot),
|
|
354
363
|
"gemini_cli" => Some(Provider::GeminiCli),
|
|
355
364
|
"fake" => Some(Provider::Fake),
|
|
356
365
|
_ => None,
|
|
@@ -362,6 +371,7 @@ pub(super) fn provider_wire(provider: Provider) -> &'static str {
|
|
|
362
371
|
Provider::Claude => "claude",
|
|
363
372
|
Provider::ClaudeCode => "claude_code",
|
|
364
373
|
Provider::Codex => "codex",
|
|
374
|
+
Provider::Copilot => "copilot",
|
|
365
375
|
Provider::GeminiCli => "gemini_cli",
|
|
366
376
|
Provider::Fake => "fake",
|
|
367
377
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
use super::*;
|
|
2
1
|
use super::common::*;
|
|
3
2
|
use super::selection::classify_restart_plan;
|
|
3
|
+
use super::*;
|
|
4
4
|
|
|
5
5
|
// ── lifecycle::restart —— 整队 Route B resume-or-fresh 重建 ──────────────────
|
|
6
6
|
|
|
@@ -101,10 +101,9 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
101
101
|
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
|
|
102
102
|
let mut state = selected.state;
|
|
103
103
|
crate::lifecycle::launch::ensure_owner_allowed_for_state(&state, None)?;
|
|
104
|
-
let spec_workspace = selected
|
|
105
|
-
.
|
|
106
|
-
|
|
107
|
-
.ok_or_else(|| LifecycleError::TeamSelect("active team spec workspace not found".to_string()))?;
|
|
104
|
+
let spec_workspace = selected.spec_workspace.as_ref().ok_or_else(|| {
|
|
105
|
+
LifecycleError::TeamSelect("active team spec workspace not found".to_string())
|
|
106
|
+
})?;
|
|
108
107
|
let spec = load_team_spec(spec_workspace)?;
|
|
109
108
|
let safety = crate::lifecycle::launch::effective_runtime_config(&spec)?;
|
|
110
109
|
let mut convergence = converge_missing_provider_sessions(
|
|
@@ -236,10 +235,20 @@ pub fn restart_with_transport_with_session_convergence_deadline(
|
|
|
236
235
|
crate::state::projection::save_team_scoped_state(&selected.run_workspace, &state)
|
|
237
236
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
238
237
|
let coordinator_started = start_coordinator_for_workspace(&selected.run_workspace)?;
|
|
238
|
+
let attach_commands = crate::tmux_backend::attach_commands_for_windows(
|
|
239
|
+
&selected.run_workspace,
|
|
240
|
+
&session_name,
|
|
241
|
+
plan.decisions
|
|
242
|
+
.iter()
|
|
243
|
+
.map(|decision| decision.agent_id.as_str()),
|
|
244
|
+
);
|
|
245
|
+
let next_actions = attach_commands.clone();
|
|
239
246
|
Ok(RestartReport::Restarted {
|
|
240
247
|
session_name,
|
|
241
248
|
agents: plan.decisions,
|
|
242
249
|
coordinator_started,
|
|
250
|
+
next_actions,
|
|
251
|
+
attach_commands,
|
|
243
252
|
})
|
|
244
253
|
}
|
|
245
254
|
|
|
@@ -456,10 +465,7 @@ fn mark_leader_receiver_rebind_required(state: &mut serde_json::Value, session_n
|
|
|
456
465
|
.and_then(|v| v.as_str())
|
|
457
466
|
.is_some_and(|status| status == "attached")
|
|
458
467
|
{
|
|
459
|
-
receiver.insert(
|
|
460
|
-
"status".to_string(),
|
|
461
|
-
serde_json::json!("rebind_required"),
|
|
462
|
-
);
|
|
468
|
+
receiver.insert("status".to_string(), serde_json::json!("rebind_required"));
|
|
463
469
|
}
|
|
464
470
|
}
|
|
465
471
|
|
|
@@ -520,11 +526,7 @@ fn mark_agent_respawned(
|
|
|
520
526
|
if let Some(pane_pid) = pane_pid {
|
|
521
527
|
agent.insert("pane_pid".to_string(), serde_json::json!(pane_pid));
|
|
522
528
|
}
|
|
523
|
-
crate::lifecycle::launch::persist_command_plan_state(
|
|
524
|
-
agent,
|
|
525
|
-
&spawn.plan,
|
|
526
|
-
&spawn.profile_launch,
|
|
527
|
-
);
|
|
529
|
+
crate::lifecycle::launch::persist_command_plan_state(agent, &spawn.plan, &spawn.profile_launch);
|
|
528
530
|
persist_effective_approval_policy_for_restart(agent, safety);
|
|
529
531
|
agent.remove("startup_prompts");
|
|
530
532
|
agent.remove("startup_prompt_status");
|
|
@@ -583,8 +585,7 @@ fn write_restart_resume_decision_event(
|
|
|
583
585
|
|
|
584
586
|
let path = workspace.join(".team").join("logs").join("events.jsonl");
|
|
585
587
|
if let Some(parent) = path.parent() {
|
|
586
|
-
std::fs::create_dir_all(parent)
|
|
587
|
-
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
588
|
+
std::fs::create_dir_all(parent).map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
588
589
|
}
|
|
589
590
|
let mut event = serde_json::json!({
|
|
590
591
|
"ts": chrono::Utc::now().to_rfc3339(),
|
|
@@ -615,8 +616,8 @@ fn write_restart_resume_decision_event(
|
|
|
615
616
|
}
|
|
616
617
|
}
|
|
617
618
|
}
|
|
618
|
-
let line =
|
|
619
|
-
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
619
|
+
let line =
|
|
620
|
+
serde_json::to_string(&event).map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
620
621
|
let mut file = std::fs::OpenOptions::new()
|
|
621
622
|
.create(true)
|
|
622
623
|
.append(true)
|
|
@@ -667,7 +668,10 @@ pub fn select_restart_state(
|
|
|
667
668
|
.get("active_team_key")
|
|
668
669
|
.and_then(serde_json::Value::as_str)
|
|
669
670
|
.filter(|s| !s.is_empty())
|
|
670
|
-
.map_or_else(
|
|
671
|
+
.map_or_else(
|
|
672
|
+
|| crate::state::projection::team_state_key(&selected),
|
|
673
|
+
str::to_string,
|
|
674
|
+
);
|
|
671
675
|
Ok(restart_candidate_from_state(workspace, &key, &selected))
|
|
672
676
|
}
|
|
673
677
|
|
|
@@ -733,12 +737,14 @@ fn restart_candidate_has_context(state: &serde_json::Value) -> bool {
|
|
|
733
737
|
.and_then(serde_json::Value::as_object)
|
|
734
738
|
.is_some_and(|agents| {
|
|
735
739
|
agents.values().any(|agent| {
|
|
736
|
-
["session_id", "rollout_path", "first_send_at"]
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
740
|
+
["session_id", "rollout_path", "first_send_at"]
|
|
741
|
+
.iter()
|
|
742
|
+
.any(|key| {
|
|
743
|
+
agent
|
|
744
|
+
.get(*key)
|
|
745
|
+
.and_then(serde_json::Value::as_str)
|
|
746
|
+
.is_some_and(|s| !s.is_empty())
|
|
747
|
+
})
|
|
742
748
|
})
|
|
743
749
|
})
|
|
744
750
|
}
|
|
@@ -90,7 +90,7 @@ fn lanea_stop_agent_unknown_agent_is_unknown_worker_not_owner_refused() {
|
|
|
90
90
|
#[test]
|
|
91
91
|
fn lanea_fork_agent_unknown_source_is_unknown_worker_before_session_check() {
|
|
92
92
|
let ws = lanea_team_ws("stopped");
|
|
93
|
-
let text = format!("{:?}", fork_agent(&ws, &aid("ghost"), &aid("newfork"), false, None));
|
|
93
|
+
let text = format!("{:?}", fork_agent(&ws, &aid("ghost"), &aid("newfork"), None, false, None));
|
|
94
94
|
assert!(
|
|
95
95
|
text.contains("unknown worker"),
|
|
96
96
|
"fork_agent must reject an UNKNOWN source as 'unknown worker agent id: ghost' BEFORE the session-id \
|
|
@@ -105,7 +105,7 @@ fn lanea_fork_agent_unknown_source_is_unknown_worker_before_session_check() {
|
|
|
105
105
|
fn lanea_fork_agent_duplicate_target_is_already_exists_before_session_check() {
|
|
106
106
|
let ws = lanea_team_ws("stopped");
|
|
107
107
|
// target 'bravo' already exists in the spec -> duplicate; source 'alpha' exists (its session_id is irrelevant).
|
|
108
|
-
let text = format!("{:?}", fork_agent(&ws, &aid("alpha"), &aid("bravo"), false, None));
|
|
108
|
+
let text = format!("{:?}", fork_agent(&ws, &aid("alpha"), &aid("bravo"), None, false, None));
|
|
109
109
|
assert!(
|
|
110
110
|
text.contains("already exists"),
|
|
111
111
|
"fork_agent must reject a DUPLICATE target 'bravo' as 'agent id already exists' BEFORE the session-id \
|
|
@@ -185,14 +185,14 @@ fn plan_condition_rejects_out_of_grammar() {
|
|
|
185
185
|
|
|
186
186
|
// ───────────────────────────────────────────────────────────────────────
|
|
187
187
|
// resolve_display_backend — display/backend.py
|
|
188
|
-
// 默认
|
|
188
|
+
// 默认 none;非默认非静默(non_default=true 触发 display.backend_resolved)。
|
|
189
189
|
// ───────────────────────────────────────────────────────────────────────
|
|
190
190
|
|
|
191
191
|
#[test]
|
|
192
|
-
fn
|
|
192
|
+
fn resolve_backend_defaults_to_none_when_none_requested() {
|
|
193
193
|
let r = resolve_display_backend(None, None);
|
|
194
|
-
assert_eq!(r.backend, DisplayBackend::
|
|
195
|
-
assert!(!r.non_default, "默认
|
|
194
|
+
assert_eq!(r.backend, DisplayBackend::None);
|
|
195
|
+
assert!(!r.non_default, "默认 none 不应标记 non_default");
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
#[test]
|
|
@@ -841,7 +841,7 @@ fn close_team_display_empty_workspace_closes_nothing_not_error() {
|
|
|
841
841
|
#[test]
|
|
842
842
|
fn fork_agent_on_unowned_workspace_does_not_silently_fork() {
|
|
843
843
|
let ws = temp_ws();
|
|
844
|
-
match fork_agent(&ws, &aid("src"), &aid("dst"), false, None) {
|
|
844
|
+
match fork_agent(&ws, &aid("src"), &aid("dst"), None, false, None) {
|
|
845
845
|
// 允许:owner 门 / team 选择 / 缺 spec(provider 命令构造前的上游门)。
|
|
846
846
|
Err(LifecycleError::OwnerRefused(_))
|
|
847
847
|
| Err(LifecycleError::TeamSelect(_))
|
|
@@ -464,7 +464,7 @@ fn lanea_remove_rollback_restores_via_spec_state_file_path() {
|
|
|
464
464
|
fn lanea_fork_dup_target_leader_id_is_already_exists() {
|
|
465
465
|
let ws = fork_ws(DELEG_ROLE_ALPHA);
|
|
466
466
|
let tx = LaneTransport::new("team-laneateam", &[]);
|
|
467
|
-
let text = format!("{:?}", fork_agent_with_transport(&ws, &aid("alpha"), &aid("leader"), false, None, &tx));
|
|
467
|
+
let text = format!("{:?}", fork_agent_with_transport(&ws, &aid("alpha"), &aid("leader"), None, false, None, &tx));
|
|
468
468
|
assert!(
|
|
469
469
|
text.contains("already exists"),
|
|
470
470
|
"golden operations.py:301-302 (_find_agent matches the leader): forking ONTO the leader id must raise \
|
|
@@ -481,7 +481,7 @@ fn lanea_fork_dup_target_leader_id_is_already_exists() {
|
|
|
481
481
|
fn lanea_fork_window_already_exists_guard_before_spec_mutation() {
|
|
482
482
|
let ws = fork_ws(DELEG_ROLE_ALPHA);
|
|
483
483
|
let tx = LaneTransport::new("team-laneateam", &["newfork"]); // the target window already exists
|
|
484
|
-
let text = format!("{:?}", fork_agent_with_transport(&ws, &aid("alpha"), &aid("newfork"), false, None, &tx));
|
|
484
|
+
let text = format!("{:?}", fork_agent_with_transport(&ws, &aid("alpha"), &aid("newfork"), None, false, None, &tx));
|
|
485
485
|
assert!(
|
|
486
486
|
text.contains("tmux window already exists for fork target: team-laneateam:newfork"),
|
|
487
487
|
"golden operations.py:310-312: a pre-existing target window must raise 'tmux window already exists for \
|
|
@@ -505,7 +505,7 @@ fn lanea_fork_window_already_exists_guard_before_spec_mutation() {
|
|
|
505
505
|
fn lanea_fork_gate_error_text_and_spec_rollback_on_adapter_arm() {
|
|
506
506
|
let ws = fork_ws(DELEG_ROLE_ALPHA_COMPAT); // source alpha auth_mode=compatible_api -> native fork unsupported
|
|
507
507
|
let tx = LaneTransport::new("team-laneateam", &[]);
|
|
508
|
-
let result = fork_agent_with_transport(&ws, &aid("alpha"), &aid("newfork"), false, None, &tx);
|
|
508
|
+
let result = fork_agent_with_transport(&ws, &aid("alpha"), &aid("newfork"), None, false, None, &tx);
|
|
509
509
|
let text = format!("{result:?}");
|
|
510
510
|
assert!(
|
|
511
511
|
text.contains("codex does not support native session fork"),
|
|
@@ -530,7 +530,7 @@ fn lanea_fork_gate_error_text_and_spec_rollback_on_adapter_arm() {
|
|
|
530
530
|
fn lanea_fork_report_session_id_is_not_pane_id() {
|
|
531
531
|
let ws = fork_ws(DELEG_ROLE_ALPHA); // codex+subscription -> native fork supported -> full success path
|
|
532
532
|
let tx = LaneTransport::new("team-laneateam", &[]);
|
|
533
|
-
let report = fork_agent_with_transport(&ws, &aid("alpha"), &aid("newfork"), false, None, &tx).expect("fork ok (codex subscription supports fork)");
|
|
533
|
+
let report = fork_agent_with_transport(&ws, &aid("alpha"), &aid("newfork"), None, false, None, &tx).expect("fork ok (codex subscription supports fork)");
|
|
534
534
|
assert_ne!(
|
|
535
535
|
report.session_id,
|
|
536
536
|
Some(crate::provider::SessionId::new("%newfork")),
|
|
@@ -226,6 +226,7 @@ fn fork_agent_proceeds_past_owner_gate_for_unowned_workspace() {
|
|
|
226
226
|
&ws,
|
|
227
227
|
&AgentId::new("w1"),
|
|
228
228
|
&AgentId::new("w1-fork"),
|
|
229
|
+
None,
|
|
229
230
|
false,
|
|
230
231
|
None,
|
|
231
232
|
&transport,
|
|
@@ -886,8 +887,8 @@ fn quick_start_state_seeds_spec_path_workspace_leader_display_backend() {
|
|
|
886
887
|
);
|
|
887
888
|
assert_eq!(
|
|
888
889
|
state["display_backend"],
|
|
889
|
-
json!("
|
|
890
|
-
"golden resolve_display_backend(None, source='launch') defaults to
|
|
890
|
+
json!("none"),
|
|
891
|
+
"golden resolve_display_backend(None, source='launch') defaults to none"
|
|
891
892
|
);
|
|
892
893
|
}
|
|
893
894
|
|
|
@@ -960,7 +961,8 @@ fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
|
|
|
960
961
|
assert!(agent["captured_at"].is_null());
|
|
961
962
|
assert!(agent["captured_via"].is_null());
|
|
962
963
|
assert!(agent["attribution_confidence"].is_null());
|
|
963
|
-
|
|
964
|
+
// D5 (#264) / Python launch/core.py:253 — fresh launch persists spawn_cwd=workspace.
|
|
965
|
+
assert_eq!(agent["spawn_cwd"], json!(workspace.to_string_lossy()));
|
|
964
966
|
assert_eq!(agent["spawned_at"], json!(FIXED_SPAWNED_AT));
|
|
965
967
|
assert_eq!(
|
|
966
968
|
agent["pane_id"],
|
|
@@ -483,6 +483,8 @@ pub enum QuickStartReport {
|
|
|
483
483
|
session_name: SessionName,
|
|
484
484
|
launch: Box<LaunchReport>,
|
|
485
485
|
next_actions: Vec<String>,
|
|
486
|
+
attach_commands: Vec<String>,
|
|
487
|
+
display_backend: String,
|
|
486
488
|
/// BUG-7: real readiness verdict. `Ready` ⇒ the wrapper completed AND the
|
|
487
489
|
/// caller already verified tool-set availability; the framework itself
|
|
488
490
|
/// never emits this without an external observable confirming worker
|
|
@@ -613,6 +615,8 @@ pub enum RestartReport {
|
|
|
613
615
|
session_name: SessionName,
|
|
614
616
|
agents: Vec<RestartedAgent>,
|
|
615
617
|
coordinator_started: bool,
|
|
618
|
+
next_actions: Vec<String>,
|
|
619
|
+
attach_commands: Vec<String>,
|
|
616
620
|
},
|
|
617
621
|
/// atomic refusal(`reason=resume_atomicity`):某 interacted worker 不可 resume
|
|
618
622
|
/// 且非 allow_fresh。**nothing created yet**,无需回滚。
|