@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.
Files changed (77) hide show
  1. package/Cargo.lock +252 -84
  2. package/Cargo.toml +3 -1
  3. package/crates/team-agent/Cargo.toml +3 -0
  4. package/crates/team-agent/src/cli/adapters.rs +119 -50
  5. package/crates/team-agent/src/cli/diagnose.rs +36 -13
  6. package/crates/team-agent/src/cli/emit.rs +173 -109
  7. package/crates/team-agent/src/cli/leader.rs +16 -0
  8. package/crates/team-agent/src/cli/leader_prompt.rs +261 -0
  9. package/crates/team-agent/src/cli/leaders.rs +32 -32
  10. package/crates/team-agent/src/cli/mod.rs +158 -14
  11. package/crates/team-agent/src/cli/models.rs +68 -18
  12. package/crates/team-agent/src/cli/named_address.rs +17 -16
  13. package/crates/team-agent/src/cli/route.rs +4 -4
  14. package/crates/team-agent/src/cli/send/presentation.rs +19 -18
  15. package/crates/team-agent/src/cli/send/resolve.rs +11 -11
  16. package/crates/team-agent/src/cli/send.rs +4 -4
  17. package/crates/team-agent/src/cli/spec.rs +35 -29
  18. package/crates/team-agent/src/cli/status.rs +7 -7
  19. package/crates/team-agent/src/cli/status_port/brief.rs +268 -101
  20. package/crates/team-agent/src/cli/tests/base.rs +94 -6
  21. package/crates/team-agent/src/cli/tests/divergence.rs +3 -2
  22. package/crates/team-agent/src/cli/tests/lane_c.rs +1 -1
  23. package/crates/team-agent/src/cli/tests/named_address.rs +3 -3
  24. package/crates/team-agent/src/cli/tests/status_send.rs +9 -7
  25. package/crates/team-agent/src/cli/triage.rs +53 -2
  26. package/crates/team-agent/src/cli/types.rs +8 -8
  27. package/crates/team-agent/src/compiler/tests.rs +12 -0
  28. package/crates/team-agent/src/compiler.rs +65 -26
  29. package/crates/team-agent/src/contract_runtime/backend.rs +646 -0
  30. package/crates/team-agent/src/contract_runtime/config.rs +309 -0
  31. package/crates/team-agent/src/contract_runtime/discovery.rs +237 -0
  32. package/crates/team-agent/src/contract_runtime/forward.rs +229 -0
  33. package/crates/team-agent/src/contract_runtime/framework.rs +261 -0
  34. package/crates/team-agent/src/contract_runtime/mcp.rs +396 -0
  35. package/crates/team-agent/src/contract_runtime/mod.rs +14 -0
  36. package/crates/team-agent/src/contract_runtime/prompt.rs +216 -0
  37. package/crates/team-agent/src/contract_runtime/pump.rs +207 -0
  38. package/crates/team-agent/src/contract_runtime/registry.rs +30 -0
  39. package/crates/team-agent/src/coordinator/tick.rs +26 -8
  40. package/crates/team-agent/src/leader/mod.rs +1 -0
  41. package/crates/team-agent/src/leader/prompt.rs +906 -0
  42. package/crates/team-agent/src/leader/start.rs +265 -16
  43. package/crates/team-agent/src/leader/tests/identity.rs +21 -0
  44. package/crates/team-agent/src/leader/tests/mod.rs +2 -0
  45. package/crates/team-agent/src/leader/tests/prompt_contract.rs +268 -0
  46. package/crates/team-agent/src/leader/types.rs +6 -0
  47. package/crates/team-agent/src/lib.rs +4 -0
  48. package/crates/team-agent/src/lifecycle/launch/identity.rs +172 -13
  49. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +760 -365
  50. package/crates/team-agent/src/lifecycle/launch/spawn.rs +38 -9
  51. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +17 -0
  52. package/crates/team-agent/src/lifecycle/launch.rs +39 -28
  53. package/crates/team-agent/src/lifecycle/profile_launch.rs +58 -46
  54. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +97 -52
  55. package/crates/team-agent/src/lifecycle/restart.rs +1 -1
  56. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -2
  57. package/crates/team-agent/src/lifecycle/types.rs +4 -1
  58. package/crates/team-agent/src/lifecycle/worker_command_context.rs +37 -27
  59. package/crates/team-agent/src/main.rs +6 -0
  60. package/crates/team-agent/src/messaging/delivery.rs +50 -1
  61. package/crates/team-agent/src/messaging/send.rs +10 -0
  62. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +131 -0
  63. package/crates/team-agent/src/model/spec.rs +24 -12
  64. package/crates/team-agent/src/provider/adapter.rs +8 -59
  65. package/crates/team-agent/src/provider/adapters/claude.rs +6 -14
  66. package/crates/team-agent/src/provider/adapters/codex.rs +8 -18
  67. package/crates/team-agent/src/provider/adapters/copilot.rs +5 -22
  68. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +8 -45
  69. package/crates/team-agent/src/provider/adapters/grok.rs +7 -31
  70. package/crates/team-agent/src/provider/adapters/pi.rs +7 -18
  71. package/crates/team-agent/src/provider/command_helpers.rs +45 -0
  72. package/crates/team-agent/src/provider/mod.rs +1 -0
  73. package/crates/team-agent/src/provider/model_catalog.rs +21 -28
  74. package/crates/team-agent/src/provider/tests/adapter.rs +70 -0
  75. package/crates/team-agent/src/provider/tests/command_helpers.rs +55 -0
  76. package/crates/team-agent/src/state/persist.rs +73 -0
  77. package/package.json +4 -4
@@ -0,0 +1,261 @@
1
+ //! Early, host-global command dispatch: never select or initialize a workspace.
2
+ use super::ExitCode;
3
+ use crate::leader::prompt::{self, Mutation, PromptError};
4
+ use serde_json::json;
5
+ use std::io::{self, Write};
6
+ use std::path::PathBuf;
7
+
8
+ pub(crate) const HELP: &str = "Purpose:\nPersist supplemental system instructions for future Team Agent Leader launches only.\nUsage: team-agent leader-prompt [show|set|append|clear] [--json] [--file PATH | -- TEXT]\n\nOptions:\nshow (default) prints the exact text; set replaces it; append joins old and new text with two newlines; clear removes it.\nset/append require exactly one TEXT token after --, or --file PATH containing UTF-8 text. No implicit stdin.\nEverything after -- is literal text, including --json and --help. Empty text and NUL are rejected.\nConfiguration: ~/.team-agent/leader-prompt.txt, independent of the workspace. Mutations do not echo the text.\nOnly new Pi, Claude, Codex and Grok Leaders apply it; Worker launches never read it. Cursor/Copilot are refused when configured.\nCodex merges explicit argv developer_instructions, but overrides that user/profile configuration slot; migrate its existing text here.\nNative core prompts and unrelated launch arguments remain unchanged. Attach does not update an existing session.\nExamples:\nteam-agent leader-prompt show\nteam-agent leader-prompt set -- 'Keep replies concise.'\nteam-agent leader-prompt clear\n\nNext Action:\nUse show to inspect the text, then start a new Leader with team-agent pi (or claude/codex/grok).\nset/append/clear affect the next new process, not running Leaders or their conversation history.";
9
+
10
+ #[derive(Clone, Copy, Debug, Eq, PartialEq)]
11
+ enum Operation {
12
+ Show,
13
+ Set,
14
+ Append,
15
+ Clear,
16
+ }
17
+ impl Operation {
18
+ fn name(self) -> &'static str {
19
+ match self {
20
+ Self::Show => "show",
21
+ Self::Set => "set",
22
+ Self::Append => "append",
23
+ Self::Clear => "clear",
24
+ }
25
+ }
26
+ }
27
+ #[derive(Debug, Eq, PartialEq)]
28
+ enum Input {
29
+ Text(String),
30
+ File(PathBuf),
31
+ }
32
+ #[derive(Debug)]
33
+ struct Request {
34
+ operation: Operation,
35
+ input: Option<Input>,
36
+ json: bool,
37
+ help: bool,
38
+ }
39
+ fn usage() -> PromptError {
40
+ PromptError::new(
41
+ "leader_prompt_invalid_input",
42
+ "Invalid or incomplete leader-prompt arguments; use one input for set/append",
43
+ None,
44
+ )
45
+ }
46
+ fn parse(args: &[String]) -> Result<Request, PromptError> {
47
+ let end = args
48
+ .iter()
49
+ .position(|arg| arg == "--")
50
+ .unwrap_or(args.len());
51
+ let mut json = false;
52
+ let mut help = false;
53
+ let mut file = None;
54
+ let mut positional = None;
55
+ let mut i = 0;
56
+ while i < end {
57
+ match args[i].as_str() {
58
+ "--json" if !json => json = true,
59
+ "--help" | "-h" if !help => help = true,
60
+ "--stdin" => {
61
+ return Err(PromptError::new(
62
+ "leader_prompt_invalid_input",
63
+ "--stdin is not supported for leader-prompt; use -- TEXT or --file PATH instead",
64
+ None,
65
+ ));
66
+ }
67
+ "--file" if file.is_none() && i + 1 < end => {
68
+ i += 1;
69
+ if matches!(args[i].as_str(), "--json" | "--help" | "-h" | "--file") {
70
+ return Err(usage());
71
+ }
72
+ file = Some(PathBuf::from(&args[i]));
73
+ }
74
+ token if !token.starts_with('-') && positional.is_none() => positional = Some(token),
75
+ _ => return Err(usage()),
76
+ }
77
+ i += 1;
78
+ }
79
+ let operation = match positional.unwrap_or("show") {
80
+ "show" => Operation::Show,
81
+ "set" => Operation::Set,
82
+ "append" => Operation::Append,
83
+ "clear" => Operation::Clear,
84
+ _ => return Err(usage()),
85
+ };
86
+ let text = if end < args.len() {
87
+ if args.len() != end + 2 {
88
+ return Err(usage());
89
+ }
90
+ Some(args[end + 1].clone())
91
+ } else {
92
+ None
93
+ };
94
+ if (file.is_some() && text.is_some()) || (help && (file.is_some() || text.is_some())) {
95
+ return Err(usage());
96
+ }
97
+ let input = file.map(Input::File).or_else(|| text.map(Input::Text));
98
+ if !help && (matches!(operation, Operation::Set | Operation::Append) != input.is_some()) {
99
+ return Err(usage());
100
+ }
101
+ Ok(Request {
102
+ operation,
103
+ input,
104
+ json,
105
+ help,
106
+ })
107
+ }
108
+ fn execute(request: Request) -> Result<(), PromptError> {
109
+ let path = prompt::config_path()?;
110
+ if request.operation == Operation::Show {
111
+ let text = prompt::read(&path)?.unwrap_or_default();
112
+ if request.json {
113
+ println!(
114
+ "{}",
115
+ json!({"configured": !text.is_empty(), "config_path": path, "prompt": text})
116
+ );
117
+ } else {
118
+ // Unlike mutation diagnostics, explicit show must preserve every byte.
119
+ io::stdout()
120
+ .write_all(text.as_bytes())
121
+ .and_then(|_| io::stdout().flush())
122
+ .map_err(|error| PromptError::io("leader_prompt_write_failed", &path, error))?;
123
+ }
124
+ return Ok(());
125
+ }
126
+ let mutation = match request.operation {
127
+ Operation::Clear => Mutation::Clear,
128
+ _ => {
129
+ let text = match request.input {
130
+ Some(Input::Text(text)) => text,
131
+ Some(Input::File(input)) => std::fs::read_to_string(&input).map_err(|error| {
132
+ PromptError::io("leader_prompt_unreadable", &input, error).at_config(&path)
133
+ })?,
134
+ None => return Err(usage()),
135
+ };
136
+ if request.operation == Operation::Set {
137
+ Mutation::Set(text)
138
+ } else {
139
+ Mutation::Append(text)
140
+ }
141
+ }
142
+ };
143
+ let configured = prompt::mutate(&path, mutation)?;
144
+ if request.json {
145
+ println!(
146
+ "{}",
147
+ json!({"operation": request.operation.name(), "configured": configured, "config_path": path})
148
+ );
149
+ } else {
150
+ println!(
151
+ "Leader prompt {}: configured={configured}; config={}",
152
+ request.operation.name(),
153
+ path.display()
154
+ );
155
+ }
156
+ Ok(())
157
+ }
158
+ pub(crate) fn run(args: &[String]) -> ExitCode {
159
+ let end = args
160
+ .iter()
161
+ .position(|arg| arg == "--")
162
+ .unwrap_or(args.len());
163
+ let as_json = args[..end].iter().any(|arg| arg == "--json");
164
+ let result = match parse(args) {
165
+ Ok(request) if request.help => {
166
+ println!("{HELP}");
167
+ return ExitCode::Ok;
168
+ }
169
+ Ok(request) => execute(request),
170
+ Err(error) => Err(error),
171
+ };
172
+ match result {
173
+ Ok(()) => ExitCode::Ok,
174
+ Err(error) => {
175
+ if as_json {
176
+ println!("{}", error.report());
177
+ } else {
178
+ eprintln!("{error}");
179
+ }
180
+ if error.reason == "leader_prompt_invalid_input" {
181
+ ExitCode::Usage
182
+ } else {
183
+ ExitCode::Error
184
+ }
185
+ }
186
+ }
187
+ }
188
+
189
+ #[cfg(test)]
190
+ #[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
191
+ mod tests {
192
+ use super::*;
193
+ fn args(tokens: &[&str]) -> Vec<String> {
194
+ tokens.iter().map(|text| text.to_string()).collect()
195
+ }
196
+ #[test]
197
+ fn leader_prompt_controls_stop_at_delimiter() {
198
+ for text in ["--json", "--help", "--file", "--stdin", "multi\nline🙂", ""] {
199
+ let request = parse(&args(&["set", "--", text])).unwrap();
200
+ assert!(!request.json && !request.help);
201
+ assert_eq!(request.input, Some(Input::Text(text.into())));
202
+ }
203
+ assert!(parse(&args(&[])).unwrap().operation == Operation::Show);
204
+ let request = parse(&args(&["--json", "append", "--file", "path with 中文"])).unwrap();
205
+ assert_eq!(
206
+ request.input,
207
+ Some(Input::File(PathBuf::from("path with 中文")))
208
+ );
209
+ assert!(request.json);
210
+ }
211
+ #[test]
212
+ fn leader_prompt_stdin_control_reports_unsupported_input_with_usage_exit() {
213
+ for tokens in [
214
+ vec!["set", "--stdin"],
215
+ vec!["set", "--json", "--stdin"],
216
+ vec!["append", "--json", "--stdin"],
217
+ ] {
218
+ let tokens = args(&tokens);
219
+ let error = parse(&tokens).unwrap_err();
220
+ assert_eq!(error.reason, "leader_prompt_invalid_input");
221
+ assert_eq!(
222
+ error.error,
223
+ "--stdin is not supported for leader-prompt; use -- TEXT or --file PATH instead"
224
+ );
225
+ assert!(error.config_path.is_none());
226
+ assert_eq!(error.report()["reason"], "leader_prompt_invalid_input");
227
+ assert_eq!(error.report()["error"], error.error);
228
+ assert!(error.to_string().contains("--stdin is not supported"));
229
+ assert_eq!(run(&tokens).code(), 2);
230
+ }
231
+ }
232
+ #[test]
233
+ fn leader_prompt_stdin_file_value_is_not_reclassified_as_a_control() {
234
+ for operation in ["set", "append"] {
235
+ let request = parse(&args(&[operation, "--file", "--stdin"])).unwrap();
236
+ assert_eq!(request.input, Some(Input::File(PathBuf::from("--stdin"))));
237
+ }
238
+ }
239
+ #[test]
240
+ fn leader_prompt_rejects_duplicate_missing_unknown_or_extra_controls() {
241
+ for tokens in [
242
+ vec!["set"],
243
+ vec!["append", "--"],
244
+ vec!["set", "--", "one", "two"],
245
+ vec!["set", "--file"],
246
+ vec!["set", "--file", "input", "--", "text"],
247
+ vec!["show", "--", "text"],
248
+ vec!["clear", "--file", "input"],
249
+ vec!["show", "--json", "--json"],
250
+ vec!["set", "--file", "one", "--file", "two"],
251
+ vec!["show", "--unknown"],
252
+ vec!["show", "clear"],
253
+ vec!["unknown", "--help"],
254
+ ] {
255
+ assert_eq!(
256
+ parse(&args(&tokens)).unwrap_err().reason,
257
+ "leader_prompt_invalid_input"
258
+ );
259
+ }
260
+ }
261
+ }
@@ -124,7 +124,7 @@ fn send_hint(entry: &LeaderRegistryEntry) -> String {
124
124
  format!(
125
125
  "team-agent send {} {}",
126
126
  quote_cli_arg(&to),
127
- quote_cli_arg("消息")
127
+ quote_cli_arg("message")
128
128
  )
129
129
  }
130
130
 
@@ -183,19 +183,19 @@ fn format_leaders_human(rows: &[LeaderRow], view: LeadersView) -> String {
183
183
  if rows.is_empty() {
184
184
  return match view {
185
185
  LeadersView::Live => {
186
- "没有可用主控;运行 team-agent doctor --workspace . 检查当前队伍。\n".to_string()
186
+ "No live leaders. Run team-agent doctor --workspace . for the current team.\n".to_string()
187
187
  }
188
- LeadersView::All => "没有已登记主控;首次协作用 team-agent pi 打开主控。\n".to_string(),
189
- LeadersView::Stale => "没有已失效的主控登记。\n".to_string(),
188
+ LeadersView::All => "No registered leaders. Open a leader with team-agent pi for first-time collaboration.\n".to_string(),
189
+ LeadersView::Stale => "No stale leader registrations.\n".to_string(),
190
190
  };
191
191
  }
192
- let mut out = String::from("主控\t状态\t项目\t队伍\t发送命令\n");
192
+ let mut out = String::from("leader\tstatus\tworkspace\tteam\tsend command\n");
193
193
  for row in rows {
194
194
  out.push_str("leader\t");
195
195
  out.push_str(match row.status {
196
- "LIVE" => "可用",
197
- "STALE" => "已失效",
198
- "AMBIGUOUS" => "需确认归属",
196
+ "LIVE" => "LIVE",
197
+ "STALE" => "STALE",
198
+ "AMBIGUOUS" => "AMBIGUOUS",
199
199
  other => other,
200
200
  });
201
201
  out.push('\t');
@@ -212,7 +212,7 @@ fn format_leaders_human(rows: &[LeaderRow], view: LeadersView) -> String {
212
212
  &row.entry.workspace,
213
213
  Some(&row.entry.team_key),
214
214
  )
215
- .unwrap_or_else(|| "请核对项目路径".to_string()),
215
+ .unwrap_or_else(|| "Verify the workspace path".to_string()),
216
216
  );
217
217
  }
218
218
  out.push('\n');
@@ -240,33 +240,33 @@ fn prune_json(report: &RegistryPruneReport) -> Value {
240
240
  }
241
241
 
242
242
  fn format_prune_human(report: &RegistryPruneReport) -> String {
243
- let mut out = format!("仅预览:{}\n", report.dry_run);
243
+ let mut out = format!("dry_run: {}\n", report.dry_run);
244
244
  for (label, items) in [
245
- ("可清理登记", &report.candidates),
246
- ("已清理", &report.removed),
247
- ("保留", &report.kept),
248
- ("跳过", &report.skipped),
249
- ("错误", &report.errors),
245
+ ("candidates", &report.candidates),
246
+ ("removed", &report.removed),
247
+ ("kept", &report.kept),
248
+ ("skipped", &report.skipped),
249
+ ("errors", &report.errors),
250
250
  ] {
251
251
  out.push_str(&format!("{label}: {}\n", items.len()));
252
252
  for item in items {
253
253
  out.push_str(" ");
254
254
  out.push_str(&item.workspace.display().to_string());
255
- out.push_str(",队伍:");
255
+ out.push_str(", team: ");
256
256
  out.push_str(&item.team_key);
257
257
  out.push_str(" [");
258
258
  out.push_str(match item.reason.as_str() {
259
- "LIVE" => "仍可用,保留登记",
260
- "team_key_not_found" => "队伍已不在项目中",
261
- "team_not_alive" => "队伍已停止",
262
- "registry_entry_missing" => "登记已不存在",
259
+ "LIVE" => "still live; registration retained",
260
+ "team_key_not_found" => "team no longer exists in the workspace",
261
+ "team_not_alive" => "team is stopped",
262
+ "registry_entry_missing" => "registration no longer exists",
263
263
  "registry_entry_changed" | "canonical_state_changed" => {
264
- "登记或队伍已变化,停止清理"
264
+ "registration or team changed; cleanup refused"
265
265
  }
266
- "canonical_state_unavailable" => "无法读取队伍状态,保留登记",
267
- "registry_lock_unavailable" => "登记正被其他操作使用,停止清理",
268
- "remove_failed" => "删除登记失败,请查看文件权限",
269
- _ => "无法安全确认该登记;请体检所选项目",
266
+ "canonical_state_unavailable" => "team state unavailable; registration retained",
267
+ "registry_lock_unavailable" => "registry lock unavailable; cleanup refused",
268
+ "remove_failed" => "registration removal failed; inspect file permissions",
269
+ _ => "registration could not be verified safely; diagnose the selected workspace",
270
270
  });
271
271
  out.push_str("]\n");
272
272
  }
@@ -337,13 +337,13 @@ mod tests {
337
337
  assert_eq!(lines.len(), 3, "one header and both selected rows: {out}");
338
338
  let header = lines[0];
339
339
  assert_eq!(header.split('\t').count(), 5);
340
- assert!(header.contains("状态") && header.contains("项目") && header.contains("队伍"));
340
+ assert!(header.contains("status") && header.contains("workspace") && header.contains("team"));
341
341
  let live: Vec<_> = lines[1].split('\t').collect();
342
342
  assert_eq!(
343
343
  &live[..4],
344
344
  &[
345
345
  "leader",
346
- "可用",
346
+ "LIVE",
347
347
  "/Volumes/nvme/Projects/讨论team-agent",
348
348
  "wiki-team"
349
349
  ]
@@ -354,7 +354,7 @@ mod tests {
354
354
  "team-agent",
355
355
  "send",
356
356
  "leader",
357
- "请完成任务并把答案回复给 leader。",
357
+ "Complete the task and reply to the leader.",
358
358
  "--workspace",
359
359
  "/Volumes/nvme/Projects/讨论team-agent",
360
360
  "--team",
@@ -364,7 +364,7 @@ mod tests {
364
364
  let stale: Vec<_> = lines[2].split('\t').collect();
365
365
  assert_eq!(
366
366
  &stale[..4],
367
- &["leader", "已失效", "/Users/alauda/stale", "old-team"]
367
+ &["leader", "STALE", "/Users/alauda/stale", "old-team"]
368
368
  );
369
369
  assert_eq!(stale[4], "-", "stale rows must never suggest a send: {out}");
370
370
  }
@@ -404,9 +404,9 @@ mod tests {
404
404
  #[test]
405
405
  fn empty_view_names_are_explicit() {
406
406
  for (view, empty, next) in [
407
- (LeadersView::Live, "没有可用主控", Some("team-agent doctor")),
408
- (LeadersView::Stale, "没有已失效的主控登记", None),
409
- (LeadersView::All, "没有已登记主控", Some("team-agent pi")),
407
+ (LeadersView::Live, "No live leaders", Some("team-agent doctor")),
408
+ (LeadersView::Stale, "No stale leader registrations", None),
409
+ (LeadersView::All, "No registered leaders", Some("team-agent pi")),
410
410
  ] {
411
411
  let out = format_leaders_human(&[], view);
412
412
  assert!(out.contains(empty) && out.ends_with('\n'), "{out}");