@team-agent/installer 0.5.112 → 0.5.114
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 +252 -84
- package/Cargo.toml +3 -1
- package/crates/team-agent/Cargo.toml +3 -0
- package/crates/team-agent/src/cli/adapters.rs +119 -50
- package/crates/team-agent/src/cli/diagnose.rs +36 -13
- package/crates/team-agent/src/cli/emit.rs +173 -109
- package/crates/team-agent/src/cli/leader.rs +16 -0
- package/crates/team-agent/src/cli/leader_prompt.rs +261 -0
- package/crates/team-agent/src/cli/leaders.rs +32 -32
- package/crates/team-agent/src/cli/mod.rs +158 -14
- package/crates/team-agent/src/cli/models.rs +68 -18
- package/crates/team-agent/src/cli/named_address.rs +17 -16
- package/crates/team-agent/src/cli/route.rs +4 -4
- package/crates/team-agent/src/cli/send/presentation.rs +19 -18
- package/crates/team-agent/src/cli/send/resolve.rs +11 -11
- package/crates/team-agent/src/cli/send.rs +4 -4
- package/crates/team-agent/src/cli/spec.rs +35 -29
- package/crates/team-agent/src/cli/status.rs +7 -7
- package/crates/team-agent/src/cli/status_port/brief.rs +268 -101
- package/crates/team-agent/src/cli/tests/base.rs +94 -6
- package/crates/team-agent/src/cli/tests/divergence.rs +3 -2
- package/crates/team-agent/src/cli/tests/lane_c.rs +1 -1
- package/crates/team-agent/src/cli/tests/named_address.rs +3 -3
- package/crates/team-agent/src/cli/tests/status_send.rs +9 -7
- package/crates/team-agent/src/cli/triage.rs +53 -2
- package/crates/team-agent/src/cli/types.rs +8 -8
- package/crates/team-agent/src/compiler/tests.rs +12 -0
- package/crates/team-agent/src/compiler.rs +65 -26
- package/crates/team-agent/src/contract_runtime/backend.rs +646 -0
- package/crates/team-agent/src/contract_runtime/config.rs +309 -0
- package/crates/team-agent/src/contract_runtime/discovery.rs +237 -0
- package/crates/team-agent/src/contract_runtime/forward.rs +229 -0
- package/crates/team-agent/src/contract_runtime/framework.rs +261 -0
- package/crates/team-agent/src/contract_runtime/mcp.rs +396 -0
- package/crates/team-agent/src/contract_runtime/mod.rs +14 -0
- package/crates/team-agent/src/contract_runtime/prompt.rs +216 -0
- package/crates/team-agent/src/contract_runtime/pump.rs +207 -0
- package/crates/team-agent/src/contract_runtime/registry.rs +30 -0
- package/crates/team-agent/src/coordinator/tick.rs +26 -8
- package/crates/team-agent/src/leader/mod.rs +1 -0
- package/crates/team-agent/src/leader/prompt.rs +906 -0
- package/crates/team-agent/src/leader/start.rs +265 -16
- package/crates/team-agent/src/leader/tests/identity.rs +21 -0
- package/crates/team-agent/src/leader/tests/mod.rs +2 -0
- package/crates/team-agent/src/leader/tests/prompt_contract.rs +268 -0
- package/crates/team-agent/src/leader/types.rs +6 -0
- package/crates/team-agent/src/lib.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/identity.rs +172 -13
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +760 -365
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +38 -9
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +17 -0
- package/crates/team-agent/src/lifecycle/launch.rs +39 -28
- package/crates/team-agent/src/lifecycle/profile_launch.rs +58 -46
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +97 -52
- package/crates/team-agent/src/lifecycle/restart.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -2
- package/crates/team-agent/src/lifecycle/types.rs +4 -1
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +37 -27
- package/crates/team-agent/src/main.rs +6 -0
- package/crates/team-agent/src/messaging/delivery.rs +50 -1
- package/crates/team-agent/src/messaging/send.rs +10 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +131 -0
- package/crates/team-agent/src/model/spec.rs +24 -12
- package/crates/team-agent/src/provider/adapter.rs +8 -59
- package/crates/team-agent/src/provider/adapters/claude.rs +6 -14
- package/crates/team-agent/src/provider/adapters/codex.rs +8 -18
- package/crates/team-agent/src/provider/adapters/copilot.rs +5 -22
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +8 -45
- package/crates/team-agent/src/provider/adapters/grok.rs +7 -31
- package/crates/team-agent/src/provider/adapters/pi.rs +7 -18
- package/crates/team-agent/src/provider/command_helpers.rs +45 -0
- package/crates/team-agent/src/provider/mod.rs +1 -0
- package/crates/team-agent/src/provider/model_catalog.rs +21 -28
- package/crates/team-agent/src/provider/tests/adapter.rs +70 -0
- package/crates/team-agent/src/provider/tests/command_helpers.rs +55 -0
- package/crates/team-agent/src/state/persist.rs +73 -0
- package/package.json +4 -4
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
//! Process-isolated launch planning, not native provider inference.
|
|
2
|
+
#![cfg(unix)]
|
|
3
|
+
#[path = "../../../tests/support/hermetic.rs"]
|
|
4
|
+
mod hermetic;
|
|
5
|
+
use crate::leader::{LeaderError, LeaderStartMode};
|
|
6
|
+
use crate::lifecycle::launch::pi_mcp::{materialize_pi_plan, PiMaterializeRequest, PiSessionScope};
|
|
7
|
+
use crate::provider::{McpConfig, Provider};
|
|
8
|
+
use hermetic::HermeticTestEnv;
|
|
9
|
+
use serde_json::json;
|
|
10
|
+
use std::fs;
|
|
11
|
+
use std::os::unix::fs::{symlink, PermissionsExt};
|
|
12
|
+
use std::path::{Path, PathBuf};
|
|
13
|
+
use std::process::Command;
|
|
14
|
+
|
|
15
|
+
const SENTINEL: &str = "LEADER_ONLY_309_SENTINEL_🙂";
|
|
16
|
+
const CHILD: &str = "TEAM_AGENT_TEST_LEADER_PROMPT_CHILD";
|
|
17
|
+
fn isolated(name: &str, body: impl FnOnce()) {
|
|
18
|
+
if std::env::var(CHILD).ok().as_deref() == Some(name) {
|
|
19
|
+
body();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
let output = Command::new(std::env::current_exe().unwrap())
|
|
23
|
+
.args([
|
|
24
|
+
"--exact",
|
|
25
|
+
&format!("leader::tests::prompt_contract::{name}"),
|
|
26
|
+
"--nocapture",
|
|
27
|
+
"--test-threads=1",
|
|
28
|
+
])
|
|
29
|
+
.env(CHILD, name)
|
|
30
|
+
.output()
|
|
31
|
+
.unwrap();
|
|
32
|
+
assert!(
|
|
33
|
+
output.status.success(),
|
|
34
|
+
"isolated test failed: {:?}\n{}\n{}",
|
|
35
|
+
output.status,
|
|
36
|
+
String::from_utf8_lossy(&output.stdout),
|
|
37
|
+
String::from_utf8_lossy(&output.stderr)
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
fn native_bin(h: &HermeticTestEnv) -> PathBuf {
|
|
41
|
+
let bin = h.workspace("bin");
|
|
42
|
+
for name in ["claude", "codex", "grok", "agent", "copilot", "tmux"] {
|
|
43
|
+
let path = bin.join(name);
|
|
44
|
+
let source = if name == "tmux" {
|
|
45
|
+
"#!/bin/sh\nif [ \"$1\" = -V ]; then echo 'tmux 3.3'; exit 0; fi\nexit 1\n"
|
|
46
|
+
} else {
|
|
47
|
+
"#!/bin/sh\nexit 77\n"
|
|
48
|
+
};
|
|
49
|
+
fs::write(&path, source).unwrap();
|
|
50
|
+
fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).unwrap();
|
|
51
|
+
}
|
|
52
|
+
let real = h.root().join("pi-real");
|
|
53
|
+
fs::write(&real, "#!/bin/sh\ncase \"$1\" in\n--version) echo '1.0.0';;\n--list-models) printf 'provider model\\nteam-agent test-model\\n';;\n*) exit 77;;\nesac\n").unwrap();
|
|
54
|
+
fs::set_permissions(&real, fs::Permissions::from_mode(0o755)).unwrap();
|
|
55
|
+
symlink(real, bin.join("pi")).unwrap();
|
|
56
|
+
bin
|
|
57
|
+
}
|
|
58
|
+
fn plan(
|
|
59
|
+
provider: Provider,
|
|
60
|
+
workspace: &Path,
|
|
61
|
+
args: &[String],
|
|
62
|
+
attach: bool,
|
|
63
|
+
) -> Result<crate::leader::LeaderStartPlan, LeaderError> {
|
|
64
|
+
crate::leader::start::leader_start_plan_after_ambient_authority_check(
|
|
65
|
+
provider, args, workspace, attach, attach, None, true,
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
fn prompt_text(argv: &[String], flag: &str) -> String {
|
|
69
|
+
let index = argv.iter().position(|arg| arg == flag).unwrap();
|
|
70
|
+
argv[index + 1].clone()
|
|
71
|
+
}
|
|
72
|
+
fn stable_argv(plan: &crate::leader::LeaderStartPlan) -> (Vec<String>, Vec<String>) {
|
|
73
|
+
// Fresh Pi session UUIDs are intentionally regenerated; all other bytes must match.
|
|
74
|
+
let normalize = |argv: &[String]| {
|
|
75
|
+
argv.iter()
|
|
76
|
+
.map(|arg| {
|
|
77
|
+
if let Some(index) = plan
|
|
78
|
+
.provider_argv
|
|
79
|
+
.iter()
|
|
80
|
+
.position(|token| token == "--session-id")
|
|
81
|
+
{
|
|
82
|
+
arg.replace(&plan.provider_argv[index + 1], "<fresh-session-id>")
|
|
83
|
+
} else {
|
|
84
|
+
arg.clone()
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
.collect()
|
|
88
|
+
};
|
|
89
|
+
(normalize(&plan.argv), normalize(&plan.provider_argv))
|
|
90
|
+
}
|
|
91
|
+
fn assert_no_payload_tree(path: &Path) {
|
|
92
|
+
for entry in fs::read_dir(path).unwrap() {
|
|
93
|
+
let path = entry.unwrap().path();
|
|
94
|
+
if path.is_dir() {
|
|
95
|
+
assert_no_payload_tree(&path);
|
|
96
|
+
} else if let Ok(bytes) = fs::read(&path) {
|
|
97
|
+
assert!(
|
|
98
|
+
!String::from_utf8_lossy(&bytes).contains(SENTINEL),
|
|
99
|
+
"payload persisted outside global file: {}",
|
|
100
|
+
path.display()
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
#[test]
|
|
106
|
+
fn leader_prompt_plans_preserve_absent_baseline_append_only_leaders_and_never_materialize_into_workers(
|
|
107
|
+
) {
|
|
108
|
+
isolated("leader_prompt_plans_preserve_absent_baseline_append_only_leaders_and_never_materialize_into_workers", || {
|
|
109
|
+
let h = HermeticTestEnv::enter("leader-prompt-plans");
|
|
110
|
+
let bin = native_bin(&h);
|
|
111
|
+
let _path = h.with_env("PATH", bin.to_str().unwrap());
|
|
112
|
+
let _routing = h.with_env("TEAM_AGENT_CLI_ARGV_ROUTING", "off");
|
|
113
|
+
let config = crate::leader::prompt::config_path().unwrap();
|
|
114
|
+
let providers = [Provider::Pi, Provider::Claude, Provider::ClaudeCode, Provider::Codex,
|
|
115
|
+
Provider::Grok, Provider::CursorAgent, Provider::Copilot];
|
|
116
|
+
for provider in providers {
|
|
117
|
+
let workspace = h.workspace(&format!("provider-{}", crate::provider::wire::provider_wire(provider)));
|
|
118
|
+
let baseline = plan(provider, &workspace, &[], false).unwrap();
|
|
119
|
+
assert!(baseline.leader_prompt.is_none());
|
|
120
|
+
fs::create_dir_all(config.parent().unwrap()).unwrap(); fs::write(&config, []).unwrap();
|
|
121
|
+
let empty = plan(provider, &workspace, &[], false).unwrap();
|
|
122
|
+
assert_eq!(stable_argv(&empty), stable_argv(&baseline), "empty configuration changed argv for {provider:?}");
|
|
123
|
+
assert_eq!(empty.leader_env, baseline.leader_env);
|
|
124
|
+
fs::write(&config, SENTINEL).unwrap();
|
|
125
|
+
if matches!(provider, Provider::CursorAgent | Provider::Copilot) {
|
|
126
|
+
let LeaderError::Prompt(error) = plan(provider, &workspace, &[], false).unwrap_err() else { panic!("typed unsupported") };
|
|
127
|
+
assert_eq!(error.reason, "leader_prompt_unsupported");
|
|
128
|
+
} else {
|
|
129
|
+
let configured = plan(provider, &workspace, &[], false).unwrap();
|
|
130
|
+
assert_eq!(configured.mode, baseline.mode);
|
|
131
|
+
assert_eq!(configured.leader_env, baseline.leader_env);
|
|
132
|
+
assert!(configured.leader_prompt.is_some());
|
|
133
|
+
assert!(configured.provider_argv.iter().any(|arg| arg.contains(SENTINEL)));
|
|
134
|
+
if provider == Provider::Pi {
|
|
135
|
+
assert_eq!(prompt_text(&configured.provider_argv, "--append-system-prompt"),
|
|
136
|
+
format!("{}\n\n{SENTINEL}", prompt_text(&baseline.provider_argv, "--append-system-prompt")));
|
|
137
|
+
}
|
|
138
|
+
// Only the process argv carries the global payload, never the wrapper/state/env.
|
|
139
|
+
assert_no_payload_tree(&workspace);
|
|
140
|
+
}
|
|
141
|
+
fs::remove_file(&config).unwrap();
|
|
142
|
+
}
|
|
143
|
+
fs::write(&config, SENTINEL).unwrap();
|
|
144
|
+
let workspace = h.workspace("worker");
|
|
145
|
+
let candidate = h.root().join("candidate"); fs::write(&candidate, "candidate").unwrap();
|
|
146
|
+
let mcp = McpConfig { raw: json!({"team_orchestrator": {
|
|
147
|
+
"command": candidate, "args": ["mcp-server"], "cwd": workspace,
|
|
148
|
+
"env": {"TEAM_AGENT_ID": "worker", "TEAM_AGENT_OWNER_TEAM_ID": "current"}
|
|
149
|
+
}}) };
|
|
150
|
+
for body in [SENTINEL.as_bytes(), &[0xff][..]] {
|
|
151
|
+
fs::write(&config, body).unwrap();
|
|
152
|
+
let worker = materialize_pi_plan(PiMaterializeRequest {
|
|
153
|
+
workspace: &workspace, team_id: "current", agent_id: "worker", model: None,
|
|
154
|
+
effort: None, system_prompt: "Compiled Worker contract; unchanged", team_mcp_tools: &["report_result"],
|
|
155
|
+
mcp_config: &mcp, session_scope: PiSessionScope::Isolated,
|
|
156
|
+
}).unwrap();
|
|
157
|
+
assert!(!worker.argv.iter().any(|arg| arg.contains(SENTINEL)));
|
|
158
|
+
assert!(!mcp.raw.to_string().contains(SENTINEL));
|
|
159
|
+
assert_no_payload_tree(&workspace);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
#[test]
|
|
164
|
+
fn leader_prompt_attach_skips_corrupt_configuration_and_all_injection_for_every_leader_entry() {
|
|
165
|
+
isolated(
|
|
166
|
+
"leader_prompt_attach_skips_corrupt_configuration_and_all_injection_for_every_leader_entry",
|
|
167
|
+
|| {
|
|
168
|
+
let h = HermeticTestEnv::enter("leader-prompt-attach");
|
|
169
|
+
let bin = native_bin(&h);
|
|
170
|
+
let _path = h.with_env("PATH", bin.to_str().unwrap());
|
|
171
|
+
let _routing = h.with_env("TEAM_AGENT_CLI_ARGV_ROUTING", "off");
|
|
172
|
+
let config = crate::leader::prompt::config_path().unwrap();
|
|
173
|
+
fs::create_dir_all(config.parent().unwrap()).unwrap();
|
|
174
|
+
for provider in [
|
|
175
|
+
Provider::Pi,
|
|
176
|
+
Provider::Claude,
|
|
177
|
+
Provider::ClaudeCode,
|
|
178
|
+
Provider::Codex,
|
|
179
|
+
Provider::Grok,
|
|
180
|
+
Provider::CursorAgent,
|
|
181
|
+
Provider::Copilot,
|
|
182
|
+
] {
|
|
183
|
+
let workspace = h.workspace(&format!("attach-{provider:?}"));
|
|
184
|
+
fs::write(&config, []).unwrap();
|
|
185
|
+
let baseline = plan(provider, &workspace, &[], true).unwrap();
|
|
186
|
+
for body in [SENTINEL.as_bytes(), &[0xff][..]] {
|
|
187
|
+
fs::write(&config, body).unwrap();
|
|
188
|
+
let attached = plan(provider, &workspace, &[], true).unwrap();
|
|
189
|
+
assert_eq!(attached.mode, LeaderStartMode::AttachExisting);
|
|
190
|
+
assert!(attached.leader_prompt.is_none());
|
|
191
|
+
assert_eq!(attached.argv, baseline.argv);
|
|
192
|
+
assert_eq!(attached.provider_argv, baseline.provider_argv);
|
|
193
|
+
assert_eq!(attached.leader_env, baseline.leader_env);
|
|
194
|
+
assert!(!workspace.join(".team").exists());
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
#[test]
|
|
201
|
+
fn leader_prompt_routes_are_merged_or_rejected_before_pi_artifacts() {
|
|
202
|
+
isolated(
|
|
203
|
+
"leader_prompt_routes_are_merged_or_rejected_before_pi_artifacts",
|
|
204
|
+
|| {
|
|
205
|
+
let h = HermeticTestEnv::enter("leader-prompt-routes");
|
|
206
|
+
let bin = native_bin(&h);
|
|
207
|
+
let _path = h.with_env("PATH", bin.to_str().unwrap());
|
|
208
|
+
let _routing = h.with_env("TEAM_AGENT_CLI_ARGV_ROUTING", "on");
|
|
209
|
+
let config = crate::leader::prompt::config_path().unwrap();
|
|
210
|
+
fs::create_dir_all(config.parent().unwrap()).unwrap();
|
|
211
|
+
fs::write(&config, SENTINEL).unwrap();
|
|
212
|
+
let route = crate::provider::argv_route::config_path().unwrap();
|
|
213
|
+
fs::write(
|
|
214
|
+
&route,
|
|
215
|
+
json!({"schema_version":1,"enabled":true,"providers":{
|
|
216
|
+
"claude":["--append-system-prompt=from route"],
|
|
217
|
+
"codex":["-c", "developer_instructions=\"from route\""],
|
|
218
|
+
"grok":["--rules", "from route"]
|
|
219
|
+
}})
|
|
220
|
+
.to_string(),
|
|
221
|
+
)
|
|
222
|
+
.unwrap();
|
|
223
|
+
for provider in [Provider::Claude, Provider::Codex, Provider::Grok] {
|
|
224
|
+
let workspace = h.workspace(&format!("route-{provider:?}"));
|
|
225
|
+
let argv = plan(provider, &workspace, &[], false)
|
|
226
|
+
.unwrap()
|
|
227
|
+
.provider_argv;
|
|
228
|
+
assert!(argv
|
|
229
|
+
.iter()
|
|
230
|
+
.any(|arg| arg.contains("from route") && arg.contains(SENTINEL)));
|
|
231
|
+
let args = match provider {
|
|
232
|
+
Provider::Codex => vec![
|
|
233
|
+
"--config".into(),
|
|
234
|
+
"developer_instructions=\"explicit\"".into(),
|
|
235
|
+
],
|
|
236
|
+
Provider::Grok => vec!["--rules".into(), "explicit".into()],
|
|
237
|
+
_ => vec!["--append-system-prompt".into(), "explicit".into()],
|
|
238
|
+
};
|
|
239
|
+
let LeaderError::Prompt(error) =
|
|
240
|
+
plan(provider, &workspace, &args, false).unwrap_err()
|
|
241
|
+
else {
|
|
242
|
+
panic!("typed duplicate refusal")
|
|
243
|
+
};
|
|
244
|
+
assert_eq!(error.reason, "leader_prompt_conflict");
|
|
245
|
+
assert!(!workspace.join(".team").exists());
|
|
246
|
+
}
|
|
247
|
+
for tokens in [
|
|
248
|
+
json!(["--append-system-prompt", "route override"]),
|
|
249
|
+
json!(["--"]),
|
|
250
|
+
] {
|
|
251
|
+
fs::write(
|
|
252
|
+
&route,
|
|
253
|
+
json!({"schema_version":1,"enabled":true,"providers":{"pi":tokens}})
|
|
254
|
+
.to_string(),
|
|
255
|
+
)
|
|
256
|
+
.unwrap();
|
|
257
|
+
let workspace = h.workspace("pi-route-refusal");
|
|
258
|
+
let LeaderError::Prompt(error) =
|
|
259
|
+
plan(Provider::Pi, &workspace, &[], false).unwrap_err()
|
|
260
|
+
else {
|
|
261
|
+
panic!("typed Pi route refusal")
|
|
262
|
+
};
|
|
263
|
+
assert_eq!(error.reason, "leader_prompt_conflict");
|
|
264
|
+
assert!(!workspace.join(".team").exists());
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
);
|
|
268
|
+
}
|
|
@@ -343,6 +343,10 @@ pub struct LeaderStartPlan {
|
|
|
343
343
|
pub argv: Vec<String>,
|
|
344
344
|
/// For managed launches, the provider command argv spawned inside the leader pane.
|
|
345
345
|
pub provider_argv: Vec<String>,
|
|
346
|
+
/// Payload-free launch snapshot; prompt-bearing diagnostics must withhold argv/output.
|
|
347
|
+
pub leader_prompt: Option<super::prompt::PromptMetadata>,
|
|
348
|
+
/// Native identity allocated before launch, not the Team Agent owner UUID.
|
|
349
|
+
pub session_capture: Option<crate::provider::CaptureSessionContext>,
|
|
346
350
|
/// For managed launches, the window that hosts the leader provider pane.
|
|
347
351
|
pub leader_window: Option<WindowName>,
|
|
348
352
|
/// True for external/current-pane leader compatibility paths.
|
|
@@ -456,6 +460,8 @@ pub enum LeaderError {
|
|
|
456
460
|
/// provider 命令未安装 / leader 启动前置失败(`leader_start_plan` raise RuntimeError)。
|
|
457
461
|
#[error("leader start error: {0}")]
|
|
458
462
|
Start(String),
|
|
463
|
+
#[error(transparent)]
|
|
464
|
+
Prompt(#[from] super::prompt::PromptError),
|
|
459
465
|
/// tmux target scan / set-environment / send 失败(transport 层冒泡)。
|
|
460
466
|
#[error("tmux error: {0}")]
|
|
461
467
|
Tmux(String),
|
|
@@ -53,6 +53,10 @@ pub mod state;
|
|
|
53
53
|
// step 6 (compiler) — TEAM.md + agents/*.md → 规范 team.spec dict(doc→spec 纯变换)。
|
|
54
54
|
pub mod compiler;
|
|
55
55
|
|
|
56
|
+
// Isolated new-provider runtime; legacy provider dispatch/state are not migrated.
|
|
57
|
+
#[cfg(unix)]
|
|
58
|
+
pub mod contract_runtime;
|
|
59
|
+
|
|
56
60
|
// Official communication-mode catalog. Configuration compilation and every
|
|
57
61
|
// future projection consumer must use this typed single source of truth.
|
|
58
62
|
pub mod communication_mode;
|
|
@@ -476,26 +476,62 @@ pub(super) fn annotate_persisted_team_depth(
|
|
|
476
476
|
}
|
|
477
477
|
|
|
478
478
|
/// ---
|
|
479
|
-
/// purpose:
|
|
480
|
-
/// returns:
|
|
479
|
+
/// purpose: 判断目标团队是否已有 worker runtime,已证明的 Leader-only 登记不算初始化
|
|
480
|
+
/// returns: 未初始化的有效 Leader-only 登记为 false;已有或无法分类的目标 runtime 为 true
|
|
481
481
|
/// ---
|
|
482
482
|
pub(super) fn runtime_state_has_quick_start_team(state: &serde_json::Value, team: &str) -> bool {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
483
|
+
let entry = state
|
|
484
|
+
.get("teams")
|
|
485
|
+
.and_then(serde_json::Value::as_object)
|
|
486
|
+
.and_then(|teams| teams.get(team));
|
|
487
|
+
if let Some(entry) = entry {
|
|
488
|
+
return !is_quick_start_leader_bootstrap(entry);
|
|
489
|
+
}
|
|
490
|
+
// Legacy aliases still identify existing/ambiguous runtime, not a proven
|
|
491
|
+
// canonical bootstrap record. Do not grant fresh creation through an alias.
|
|
492
|
+
if state
|
|
493
|
+
.get("teams")
|
|
494
|
+
.and_then(serde_json::Value::as_object)
|
|
495
|
+
.is_some_and(|teams| teams.values().any(|entry| json_team_identity_matches(entry, team)))
|
|
496
|
+
{
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
let matches = explicit_active_team_key(state).as_deref() == Some(team)
|
|
493
500
|
|| crate::state::projection::team_state_key(state) == team
|
|
494
501
|
|| json_team_identity_matches(state, team)
|
|
495
502
|
|| state
|
|
496
503
|
.get("session_name")
|
|
497
504
|
.and_then(serde_json::Value::as_str)
|
|
498
|
-
.is_some_and(|session| session == team || session.strip_prefix("team-") == Some(team))
|
|
505
|
+
.is_some_and(|session| session == team || session.strip_prefix("team-") == Some(team));
|
|
506
|
+
matches && !is_quick_start_leader_bootstrap(state)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// An empty agent table alone is not fresh: stopped/removed/partial teams keep
|
|
510
|
+
// their spec, session or task history. Only a coherent attached leader binding
|
|
511
|
+
// with no worker initialization evidence may proceed through first creation.
|
|
512
|
+
pub(super) fn is_quick_start_leader_bootstrap(state: &serde_json::Value) -> bool {
|
|
513
|
+
if !state.is_object()
|
|
514
|
+
|| state.get("teams").is_some_and(|teams| !teams.is_object())
|
|
515
|
+
|| !state.get("agents").and_then(serde_json::Value::as_object).is_some_and(|agents| agents.is_empty())
|
|
516
|
+
|| state.get("spec_path").is_some()
|
|
517
|
+
|| state.get("team_dir").is_some_and(|dir| !dir.is_null())
|
|
518
|
+
|| state.get("session_name").is_some_and(|session| !session.is_null())
|
|
519
|
+
|| state.get("tasks").is_some_and(|tasks| {
|
|
520
|
+
!tasks.as_object().is_some_and(|tasks| tasks.is_empty())
|
|
521
|
+
&& !tasks.as_array().is_some_and(|tasks| tasks.is_empty())
|
|
522
|
+
})
|
|
523
|
+
|| state.get("status").is_some_and(|status| status.as_str() != Some("alive"))
|
|
524
|
+
|| state.get("archived_at").is_some_and(|value| !value.is_null())
|
|
525
|
+
{
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
let owner = state.get("team_owner");
|
|
529
|
+
let receiver = state.get("leader_receiver");
|
|
530
|
+
let owner_pane = owner.and_then(|owner| owner.get("pane_id")).and_then(serde_json::Value::as_str).filter(|pane| !pane.is_empty());
|
|
531
|
+
let receiver_pane = receiver.and_then(|receiver| receiver.get("pane_id")).and_then(serde_json::Value::as_str);
|
|
532
|
+
owner_pane.is_some()
|
|
533
|
+
&& owner_pane == receiver_pane
|
|
534
|
+
&& receiver.and_then(|receiver| receiver.get("status")).and_then(serde_json::Value::as_str) == Some("attached")
|
|
499
535
|
}
|
|
500
536
|
|
|
501
537
|
/// ---
|
|
@@ -513,3 +549,126 @@ pub(super) fn json_team_identity_matches(state: &serde_json::Value, team: &str)
|
|
|
513
549
|
.and_then(serde_json::Value::as_str)
|
|
514
550
|
.is_some_and(|value| value == team)
|
|
515
551
|
}
|
|
552
|
+
|
|
553
|
+
#[cfg(test)]
|
|
554
|
+
mod quick_start_runtime_identity_tests {
|
|
555
|
+
use super::runtime_state_has_quick_start_team;
|
|
556
|
+
use serde_json::{json, Value};
|
|
557
|
+
|
|
558
|
+
fn leader_bootstrap() -> Value {
|
|
559
|
+
json!({
|
|
560
|
+
"agents": {},
|
|
561
|
+
"session_name": null,
|
|
562
|
+
"tasks": {},
|
|
563
|
+
"team_owner": {"pane_id": "%0", "provider": "pi", "owner_epoch": 1},
|
|
564
|
+
"leader_receiver": {"pane_id": "%0", "status": "attached", "session_name": "team-agent-leader-pi-fixture"},
|
|
565
|
+
"leader_client": {"diagnostic_only": true}
|
|
566
|
+
})
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
#[test]
|
|
570
|
+
fn current_launcher_seed_is_not_initialized_and_is_not_mutated() {
|
|
571
|
+
let seed = leader_bootstrap();
|
|
572
|
+
let state = json!({"active_team_key": "current", "team_key": "current", "teams": {"current": seed}});
|
|
573
|
+
let before = state.clone();
|
|
574
|
+
assert!(!runtime_state_has_quick_start_team(&state, "current"));
|
|
575
|
+
assert_eq!(state, before);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
#[test]
|
|
579
|
+
fn real_native_managed_launcher_empty_array_shape_is_fresh_and_unchanged() {
|
|
580
|
+
let mut entry = leader_bootstrap();
|
|
581
|
+
entry["tasks"] = json!([]);
|
|
582
|
+
entry["is_external_leader"] = json!(false);
|
|
583
|
+
entry["team_key"] = json!("current");
|
|
584
|
+
entry["tmux_socket"] = json!("/private/tmp/fixture-native-leader");
|
|
585
|
+
entry["owner_epoch"] = json!(1);
|
|
586
|
+
entry["team_owner"]["machine_fingerprint"] = json!("");
|
|
587
|
+
entry["team_owner"]["leader_session_uuid"] = json!("fixture-native-uuid");
|
|
588
|
+
entry["leader_receiver"]["owner_epoch"] = json!(1);
|
|
589
|
+
entry["leader_receiver"]["leader_session_uuid"] = json!("fixture-native-uuid");
|
|
590
|
+
let state = json!({
|
|
591
|
+
"agents": {}, "tasks": [], "session_name": null,
|
|
592
|
+
"active_team_key": "current", "team_key": "current",
|
|
593
|
+
"teams": {"current": entry}
|
|
594
|
+
});
|
|
595
|
+
let before = state.clone();
|
|
596
|
+
assert!(!runtime_state_has_quick_start_team(&state, "current"));
|
|
597
|
+
assert_eq!(state, before);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
#[test]
|
|
601
|
+
fn legacy_flat_leader_binding_can_initialize() {
|
|
602
|
+
let mut state = leader_bootstrap();
|
|
603
|
+
state["active_team_key"] = json!("current");
|
|
604
|
+
assert!(!runtime_state_has_quick_start_team(&state, "current"));
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
#[test]
|
|
608
|
+
fn stopped_removed_partial_and_unknown_teams_remain_existing() {
|
|
609
|
+
let cases = [
|
|
610
|
+
("agents", json!({"worker": {"status": "stopped"}})),
|
|
611
|
+
("spec_path", json!("/tmp/current/team.spec.yaml")),
|
|
612
|
+
("spec_path", Value::Null),
|
|
613
|
+
("team_dir", json!("/tmp/.team/current")),
|
|
614
|
+
("session_name", json!("team-current")),
|
|
615
|
+
("session_name", json!("team-agent-leader-stale-worker-field")),
|
|
616
|
+
("tasks", json!({"finished_task": {"status": "done"}})),
|
|
617
|
+
("tasks", json!([{"status": "done"}])),
|
|
618
|
+
("tasks", Value::Null),
|
|
619
|
+
("tasks", json!("unknown")),
|
|
620
|
+
("tasks", json!(false)),
|
|
621
|
+
("tasks", json!(0)),
|
|
622
|
+
("status", json!("stopped")),
|
|
623
|
+
("archived_at", json!("2026-10-08")),
|
|
624
|
+
("agents", Value::Null),
|
|
625
|
+
("agents", json!([])),
|
|
626
|
+
("team_owner", Value::Null),
|
|
627
|
+
("leader_receiver", json!({"pane_id": "%99", "status": "attached"})),
|
|
628
|
+
("leader_receiver", json!({"pane_id": "%0", "status": "pending"})),
|
|
629
|
+
];
|
|
630
|
+
for (key, value) in cases {
|
|
631
|
+
let mut entry = leader_bootstrap();
|
|
632
|
+
entry[key] = value;
|
|
633
|
+
let state = json!({"active_team_key": "current", "teams": {"current": entry}});
|
|
634
|
+
assert!(runtime_state_has_quick_start_team(&state, "current"), "must protect {key}: {state}");
|
|
635
|
+
}
|
|
636
|
+
for entry in [json!({}), Value::Null, json!({"agents": {}, "session_name": null})] {
|
|
637
|
+
let state = json!({"teams": {"current": entry}});
|
|
638
|
+
assert!(runtime_state_has_quick_start_team(&state, "current"));
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
#[test]
|
|
643
|
+
fn malformed_team_tables_do_not_allow_flat_bootstrap_to_bypass_the_guard() {
|
|
644
|
+
for teams in [Value::Null, json!([]), json!("damaged")] {
|
|
645
|
+
let mut state = leader_bootstrap();
|
|
646
|
+
state["active_team_key"] = json!("current");
|
|
647
|
+
state["teams"] = teams;
|
|
648
|
+
assert!(runtime_state_has_quick_start_team(&state, "current"));
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
#[test]
|
|
653
|
+
fn only_the_requested_team_supplies_initialization_evidence() {
|
|
654
|
+
let state = json!({
|
|
655
|
+
"active_team_key": "other", "spec_path": "/tmp/other/spec.yaml",
|
|
656
|
+
"session_name": "team-other", "agents": {"other_worker": {}},
|
|
657
|
+
"teams": {
|
|
658
|
+
"other": {"spec_path": "/tmp/other/spec.yaml", "agents": {"other_worker": {}}},
|
|
659
|
+
"current": leader_bootstrap()
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
assert!(!runtime_state_has_quick_start_team(&state, "current"));
|
|
663
|
+
assert!(runtime_state_has_quick_start_team(&state, "other"));
|
|
664
|
+
assert!(!runtime_state_has_quick_start_team(&state, "new-team"));
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
#[test]
|
|
668
|
+
fn ambiguous_legacy_identity_is_not_fresh() {
|
|
669
|
+
let mut first = leader_bootstrap();
|
|
670
|
+
first["name"] = json!("current");
|
|
671
|
+
let state = json!({"teams": {"first": first, "second": {"name": "current"}}});
|
|
672
|
+
assert!(runtime_state_has_quick_start_team(&state, "current"));
|
|
673
|
+
}
|
|
674
|
+
}
|