@team-agent/installer 0.5.75 → 0.5.78

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 (98) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/README.md +9 -0
  4. package/README.zh.md +8 -0
  5. package/crates/team-agent/src/cli/adapters.rs +184 -125
  6. package/crates/team-agent/src/cli/diagnose.rs +333 -68
  7. package/crates/team-agent/src/cli/emit.rs +59 -15
  8. package/crates/team-agent/src/cli/mod.rs +600 -134
  9. package/crates/team-agent/src/cli/models.rs +485 -44
  10. package/crates/team-agent/src/cli/named_address.rs +19 -5
  11. package/crates/team-agent/src/cli/profile.rs +35 -4
  12. package/crates/team-agent/src/cli/spec.rs +7 -2
  13. package/crates/team-agent/src/cli/status_port/brief.rs +1137 -0
  14. package/crates/team-agent/src/cli/status_port/compact.rs +12 -8
  15. package/crates/team-agent/src/cli/status_port/snapshot.rs +15 -12
  16. package/crates/team-agent/src/cli/status_port/store.rs +1 -1
  17. package/crates/team-agent/src/cli/status_port.rs +8 -0
  18. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +62 -8
  19. package/crates/team-agent/src/cli/tests/mod.rs +1 -0
  20. package/crates/team-agent/src/cli/tests/named_address.rs +70 -0
  21. package/crates/team-agent/src/cli/tests/run_delegation.rs +29 -24
  22. package/crates/team-agent/src/cli/tests/status_brief.rs +691 -0
  23. package/crates/team-agent/src/cli/tests/status_send.rs +8 -2
  24. package/crates/team-agent/src/cli/tests/verb_profile.rs +81 -6
  25. package/crates/team-agent/src/cli/types.rs +1 -0
  26. package/crates/team-agent/src/leader/lease.rs +538 -41
  27. package/crates/team-agent/src/leader/owner_bind.rs +40 -12
  28. package/crates/team-agent/src/leader/rediscover/tests.rs +65 -0
  29. package/crates/team-agent/src/leader/rediscover.rs +36 -6
  30. package/crates/team-agent/src/leader/start.rs +151 -14
  31. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +15 -9
  32. package/crates/team-agent/src/leader/types.rs +4 -0
  33. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +28 -227
  34. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +0 -18
  35. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +94 -227
  36. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +59 -10
  37. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +138 -14
  38. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1449 -282
  39. package/crates/team-agent/src/lifecycle/launch/readiness.rs +349 -4
  40. package/crates/team-agent/src/lifecycle/launch/spawn.rs +11 -11
  41. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +35 -3
  42. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +16 -0
  43. package/crates/team-agent/src/lifecycle/launch.rs +30 -7
  44. package/crates/team-agent/src/lifecycle/profile_launch.rs +37 -3
  45. package/crates/team-agent/src/lifecycle/restart/agent.rs +112 -100
  46. package/crates/team-agent/src/lifecycle/restart/common.rs +82 -52
  47. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +1002 -65
  48. package/crates/team-agent/src/lifecycle/restart.rs +2 -2
  49. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +3 -0
  50. package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +45 -14
  51. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +15 -0
  52. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +5 -0
  53. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +11 -13
  54. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +119 -3
  55. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +12 -0
  56. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +22 -0
  57. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +12 -0
  58. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +265 -198
  59. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +14 -30
  60. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +3 -0
  61. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +2 -5
  62. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +10 -0
  63. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +58 -35
  64. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +10 -0
  65. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +9 -0
  66. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +4 -3
  67. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -168
  68. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +145 -212
  69. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +16 -2
  70. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +69 -28
  71. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +78 -0
  72. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +3 -0
  73. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +51 -10
  74. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +22 -10
  75. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +50 -7
  76. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +137 -0
  77. package/crates/team-agent/src/lifecycle/types.rs +12 -0
  78. package/crates/team-agent/src/mcp_server/wire.rs +140 -11
  79. package/crates/team-agent/src/messaging/delivery.rs +67 -8
  80. package/crates/team-agent/src/messaging/leader_channel.rs +56 -0
  81. package/crates/team-agent/src/messaging/tests/runtime.rs +10 -2
  82. package/crates/team-agent/src/messaging/watchers.rs +48 -4
  83. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +2 -5
  84. package/crates/team-agent/src/state/paths.rs +17 -2
  85. package/crates/team-agent/src/state/persist.rs +97 -1
  86. package/crates/team-agent/src/state/projection.rs +18 -2
  87. package/crates/team-agent/src/state/repository.rs +29 -0
  88. package/crates/team-agent/src/state/selector.rs +23 -3
  89. package/crates/team-agent/src/test_support/models_timeout_fixture.rs +144 -0
  90. package/crates/team-agent/src/tmux_backend/tests.rs +121 -4
  91. package/crates/team-agent/src/tmux_backend.rs +208 -118
  92. package/crates/team-agent/src/transport.rs +72 -0
  93. package/crates/team-agent/src/transport_factory.rs +53 -0
  94. package/package.json +4 -5
  95. package/skills/team-agent/SKILL.md +45 -17
  96. package/skills/team-agent/command-coverage.json +4 -29
  97. package/npm/check-skill-links.mjs +0 -88
  98. package/skills/team-agent/references/team-agent-operator.md +0 -672
package/Cargo.lock CHANGED
@@ -581,7 +581,7 @@ dependencies = [
581
581
 
582
582
  [[package]]
583
583
  name = "team-agent"
584
- version = "0.5.75"
584
+ version = "0.5.78"
585
585
  dependencies = [
586
586
  "anyhow",
587
587
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent", "crates/win-conpty-phase0", "crates/conpty-trans
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.5.75"
12
+ version = "0.5.78"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
package/README.md CHANGED
@@ -82,6 +82,15 @@ Subagents are one-shot, fire-and-forget, and can't talk to each other. Team Agen
82
82
 
83
83
  ## Status
84
84
 
85
+ `team-agent status` prints one concise row per registered node with seven fields:
86
+ `name`, `provider`, `runtime_status`, `activity`, `health`, `session_name`, and `tmux_command`.
87
+ `--json` exposes the same projection as structured JSON. Missing or conflicting evidence is
88
+ reported as `unknown`; a non-null `tmux_command` is safe to copy for the observed socket,
89
+ session, window, and pane. `--summary` and `--detail` remain accepted for compatibility and
90
+ do not add diagnostic or history fields. The accepted nodeprobe pairing and
91
+ operator-trusted capability receipt are documented in
92
+ [`docs/status-nodeprobe.md`](docs/status-nodeprobe.md).
93
+
85
94
  **Beta.** Used daily in real projects. Rough edges possible in uncommon configurations. Issues and PRs welcome.
86
95
 
87
96
  ## License
package/README.zh.md CHANGED
@@ -82,6 +82,14 @@ subagent 是一次性的、干完即焚、彼此不通信。Team Agent 的队友
82
82
 
83
83
  ## 状态
84
84
 
85
+ `team-agent status` 为每个已登记节点输出七个简要字段:
86
+ `name`、`provider`、`runtime_status`、`activity`、`health`、`session_name`、`tmux_command`。
87
+ `--json` 与人读输出使用同一信息投影。证据缺失或冲突时显示 `unknown`;非空
88
+ `tmux_command` 可复制到已观测的 socket、session、window 和 pane。`--summary` 与
89
+ `--detail` 保留兼容性,但不会增加诊断或历史字段。已验收 nodeprobe 的配对方式、
90
+ operator-trusted 能力 receipt 与 fail-closed 边界见
91
+ [`docs/status-nodeprobe.md`](docs/status-nodeprobe.md)。
92
+
85
93
  **Beta。** 已在真实项目里日常使用,小众配置下可能有毛边。欢迎 issue 和 PR。
86
94
 
87
95
  ## License
@@ -183,10 +183,18 @@ fn quickstart_human(value: &Value) -> String {
183
183
  append_reminder(out, crate::cli::QUICK_START_REMINDER)
184
184
  }
185
185
 
186
- fn append_send_guidance(value: &mut Value, workspace: &Path, team: Option<&str>) {
187
- if value.get("ok").and_then(Value::as_bool) != Some(true) {
186
+ pub(crate) fn append_send_guidance(value: &mut Value, workspace: &Path, team: Option<&str>) {
187
+ let existing = value.get("summary").and_then(Value::as_str) == Some("existing runtime");
188
+ if value.get("ok").and_then(Value::as_bool) != Some(true) && !existing {
188
189
  return;
189
190
  }
191
+ let team = value
192
+ .get("team")
193
+ .and_then(Value::as_str)
194
+ .filter(|team| !team.is_empty())
195
+ .map(str::to_string)
196
+ .or_else(|| team.map(str::to_string));
197
+ let team = team.as_deref();
190
198
  let agents = value
191
199
  .get("agent_ids")
192
200
  .and_then(Value::as_array)
@@ -204,11 +212,36 @@ fn append_send_guidance(value: &mut Value, workspace: &Path, team: Option<&str>)
204
212
  }
205
213
  }
206
214
 
215
+ #[cfg(test)]
216
+ pub(crate) fn split_shell_argv(command: &str) -> Vec<String> {
217
+ let mut argv = Vec::new();
218
+ let mut current = String::new();
219
+ let mut chars = command.chars().peekable();
220
+ let mut quote: Option<char> = None;
221
+ while let Some(ch) = chars.next() {
222
+ match (quote, ch) {
223
+ (None, '\'') | (None, '"') => quote = Some(ch),
224
+ (Some(q), c) if c == q => quote = None,
225
+ (None, c) if c.is_whitespace() => {
226
+ if !current.is_empty() {
227
+ argv.push(std::mem::take(&mut current));
228
+ }
229
+ }
230
+ (_, c) => current.push(c),
231
+ }
232
+ }
233
+ if !current.is_empty() {
234
+ argv.push(current);
235
+ }
236
+ argv
237
+ }
238
+
207
239
  pub(crate) fn send_command(agent: &str, workspace: &Path, team: Option<&str>) -> Option<String> {
208
240
  let workspace = workspace.to_str()?;
209
241
  let mut command = format!(
210
- "team-agent send {} \\\"MESSAGE\\\" --workspace {}",
242
+ "team-agent send {} {} --workspace {}",
211
243
  shell_quote(agent),
244
+ shell_quote("MESSAGE"),
212
245
  shell_quote(workspace)
213
246
  );
214
247
  if let Some(team) = team {
@@ -280,8 +313,8 @@ fn warn_ignored_owner_team_id(team_dir: &std::path::Path) {
280
313
  }
281
314
  }
282
315
 
283
- /// `cmd_status`(`commands.py:90`)。三态:`--summary`(xor json,xor agent)→五行文本;
284
- /// `--json`→`status_port::status(compact=!detail)`;else→`status_port::format_status(agent)`。
316
+ /// `cmd_status`(`commands.py:90`)。CLI status 统一为只读七字段 brief;
317
+ /// `--json` 与人读路径共享 nodeprobe-backed projection;`--summary`/`--detail` 保留参数兼容性但不暴露诊断。
285
318
  #[cfg(test)]
286
319
  pub(crate) fn cmd_status(args: &StatusArgs) -> Result<CmdResult, CliError> {
287
320
  cmd_status_for_team(args, args.team.as_deref())
@@ -300,123 +333,50 @@ pub fn cmd_status_for_team(args: &StatusArgs, team: Option<&str>) -> Result<CmdR
300
333
  }
301
334
  // S4QR-001 (0.4.8): selected-team ambiguity gate. status is a selected-team
302
335
  // command: when the workspace has 2+ alive teams and no --team was passed,
303
- // refuse instead of silently defaulting to the active team. Uses the same
304
- // CommandScope::resolve helper as refuse_if_multi_alive_team_missing_scope
305
- // (cli/emit.rs:964-979) so the destructive-command ambiguity gate and the
306
- // selected-team-read ambiguity gate share a single source of truth.
336
+ // refuse instead of silently defaulting to the active team. Uses the
337
+ // read-only CommandScope helper so the gate cannot migrate/write state.
307
338
  if team.is_none() {
308
- let scope = crate::state::paths::CommandScope::resolve(&args.workspace, None);
339
+ let scope = crate::state::paths::CommandScope::resolve_readonly(&args.workspace, None);
309
340
  if scope.is_ambiguous() {
310
341
  let candidates: Vec<String> = scope.candidates().to_vec();
311
342
  let message = format!(
312
343
  "status: workspace has multiple alive teams ({}); pass `--team <key>` to choose one",
313
344
  candidates.join(", ")
314
345
  );
315
- if args.json {
316
- let payload = serde_json::json!({
317
- "ok": false,
318
- "status": "refused",
319
- "reason": "team_target_ambiguous",
320
- "candidates": candidates,
321
- "message": message,
322
- });
323
- return Ok(CmdResult::from_json(payload, args.json));
324
- }
325
346
  return Err(CliError::Usage(message));
326
347
  }
327
348
  }
328
- let selected = match crate::state::selector::resolve_active_team(
349
+ let selected = crate::state::selector::resolve_active_team_readonly(
329
350
  &args.workspace,
330
351
  team,
331
352
  crate::state::selector::SelectorMode::RuntimeOnly,
332
- ) {
333
- Ok(selected) => selected,
334
- Err(error) => {
335
- return Ok(CmdResult::from_json(
336
- status_selector_error_payload(&error.to_string(), &args.workspace),
337
- args.json,
338
- ));
353
+ )?;
354
+ if let Some(agent) = args.agent.as_deref() {
355
+ if !status_port::registered_agent_exists(&selected.state, agent) {
356
+ return Err(CliError::Runtime(format!("unknown agent id: {agent}")));
339
357
  }
340
- };
341
- if args.summary {
342
- // 0.4.x compact slimming: `--summary` renders the five-line triage
343
- // text via `format_status_summary`, which needs the full payload
344
- // fields (coordinator, leader_receiver, agent_health, queued_messages,
345
- // latest_results). The slim compact projection drops those, so the
346
- // summary path must request the full payload (compact=false).
347
- let value = status_port::status_scoped(
348
- &selected.run_workspace,
349
- &selected.state,
350
- Some(&selected.team_key),
351
- false,
352
- false,
353
- )?;
354
- return Ok(CmdResult::human(append_reminder(
355
- format_status_summary(&value),
356
- crate::cli::STATUS_REMINDER,
357
- )));
358
358
  }
359
+ // Status brief is deliberately independent of the legacy RuntimeSnapshot:
360
+ // it performs one bounded nodeprobe sample and exposes exactly seven
361
+ // fields. `--summary` and `--detail` remain parser-compatible but do not
362
+ // re-enable history, runtime diagnostics, or reminder text.
359
363
  if args.json {
360
- let value = status_port::status_scoped(
361
- &selected.run_workspace,
362
- &selected.state,
363
- Some(&selected.team_key),
364
- status_compact_flag(args.detail),
365
- args.detail,
366
- )?;
367
- return Ok(CmdResult::from_json(value, true));
364
+ return Ok(CmdResult::from_json(
365
+ status_port::status_brief_scoped(
366
+ &selected.run_workspace,
367
+ &selected.state,
368
+ args.agent.as_deref(),
369
+ ),
370
+ true,
371
+ ));
368
372
  }
369
- let mut text = status_port::format_status_scoped(
373
+ Ok(CmdResult::human(status_port::format_status_brief(
370
374
  &selected.run_workspace,
371
375
  &selected.state,
372
- Some(&selected.team_key),
373
376
  args.agent.as_deref(),
374
- )?;
375
- if args.detail {
376
- let value = status_port::status_scoped(
377
- &selected.run_workspace,
378
- &selected.state,
379
- Some(&selected.team_key),
380
- false,
381
- true,
382
- )?;
383
- if let Some(hint) = value
384
- .pointer("/runtime/hint")
385
- .and_then(serde_json::Value::as_str)
386
- .filter(|hint| !hint.is_empty())
387
- {
388
- if !text.is_empty() {
389
- text.push('\n');
390
- }
391
- text.push_str(hint);
392
- }
393
- }
394
- Ok(CmdResult::human(append_reminder(
395
- text,
396
- crate::cli::STATUS_REMINDER,
397
377
  )))
398
378
  }
399
379
 
400
- fn status_selector_error_payload(error: &str, workspace: &Path) -> Value {
401
- let stamp = chrono::Utc::now().format("%Y%m%d-%H%M%S%.6f");
402
- let log_path = std::env::temp_dir()
403
- .join("team-agent")
404
- .join("cli-errors")
405
- .join(format!("status-{stamp}.log"));
406
- if let Some(parent) = log_path.parent() {
407
- let _ = std::fs::create_dir_all(parent);
408
- }
409
- let _ = std::fs::write(&log_path, format!("{error}\n"));
410
- json!({
411
- "ok": false,
412
- "error": error,
413
- "action": "run `team-agent doctor` or inspect the log path shown here",
414
- "log": log_path.to_string_lossy().to_string(),
415
- "workspace": workspace.to_string_lossy().to_string(),
416
- "reminder": crate::cli::STATUS_REMINDER,
417
- })
418
- }
419
-
420
380
  /// `cmd_watch`(`commands.py:103`)。委派 `coordinator::run_watch`;KeyboardInterrupt/正常 → `SystemExit(0)`。
421
381
  /// 返回 [`CmdResult::none`](不经 emit)。
422
382
  pub fn cmd_watch(args: &WatchArgs) -> Result<CmdResult, CliError> {
@@ -601,6 +561,42 @@ pub fn cmd_allow_peer_talk(args: &AllowPeerTalkArgs) -> Result<CmdResult, CliErr
601
561
 
602
562
  /// `cmd_diagnose`(`parser.py:298`)。
603
563
  pub fn cmd_diagnose(args: &DiagnoseArgs) -> Result<CmdResult, CliError> {
564
+ let team_hint = args
565
+ .team
566
+ .as_deref()
567
+ .filter(|team| !team.is_empty())
568
+ .unwrap_or("current");
569
+ // Check the caller-supplied workspace before RuntimeOnly synthesis or
570
+ // transport setup, which can create `.team` paths and make an empty
571
+ // directory look like an unbound team.
572
+ if !crate::cli::diagnose::workspace_has_existing_team_runtime(&args.workspace, team_hint) {
573
+ let (issues, suggested_repairs) =
574
+ crate::cli::diagnose::missing_team_runtime_issues_and_repairs(team_hint);
575
+ let event_log = args
576
+ .workspace
577
+ .join(".team")
578
+ .join("logs")
579
+ .join("events.jsonl");
580
+ return Ok(CmdResult::from_json(
581
+ json!({
582
+ "event_log": event_log.to_string_lossy().to_string(),
583
+ "issues": issues,
584
+ "ok": false,
585
+ "providers": provider_doctor_checks(),
586
+ "runtime": {
587
+ "workspace": args.workspace.to_string_lossy().to_string(),
588
+ "team_key": team_hint,
589
+ "session_name": Value::Null,
590
+ "leader_receiver": Value::Null,
591
+ "agent_count": 0,
592
+ "message_count": 0,
593
+ "result_count": 0,
594
+ },
595
+ "suggested_repairs": suggested_repairs,
596
+ }),
597
+ args.json,
598
+ ));
599
+ }
604
600
  let selected = crate::state::selector::resolve_active_team(
605
601
  &args.workspace,
606
602
  args.team.as_deref(),
@@ -629,7 +625,12 @@ pub fn cmd_diagnose(args: &DiagnoseArgs) -> Result<CmdResult, CliError> {
629
625
  )),
630
626
  };
631
627
  let (issues, suggested_repairs) =
632
- diagnose_runtime_for_workspace(&selected.run_workspace, &state, backend.as_ref());
628
+ diagnose_runtime_for_workspace(
629
+ &selected.run_workspace,
630
+ &state,
631
+ backend.as_ref(),
632
+ Some(selected.team_key.as_str()),
633
+ );
633
634
  let ok = issues.as_array().is_some_and(Vec::is_empty);
634
635
  Ok(CmdResult::from_json(
635
636
  json!({
@@ -1420,17 +1421,24 @@ pub fn cmd_restart(args: &RestartArgs) -> Result<CmdResult, CliError> {
1420
1421
 
1421
1422
  /// `cmd_start_agent`(`commands.py:348`)。
1422
1423
  pub fn cmd_start_agent(args: &StartAgentArgs) -> Result<CmdResult, CliError> {
1423
- Ok(CmdResult::from_json(
1424
- lifecycle_port::start_agent(
1425
- &args.workspace,
1426
- &args.agent,
1427
- args.force,
1428
- !args.no_display,
1429
- args.allow_fresh,
1430
- args.team.as_deref(),
1431
- )?,
1432
- args.json,
1433
- ))
1424
+ let mut value = lifecycle_port::start_agent(
1425
+ &args.workspace,
1426
+ &args.agent,
1427
+ args.force,
1428
+ !args.no_display,
1429
+ args.allow_fresh,
1430
+ args.team.as_deref(),
1431
+ )?;
1432
+ if value.get("agent_ids").is_none() {
1433
+ if let Some(object) = value.as_object_mut() {
1434
+ object.insert(
1435
+ "agent_ids".to_string(),
1436
+ json!([args.agent.as_str()]),
1437
+ );
1438
+ }
1439
+ }
1440
+ append_send_guidance(&mut value, &args.workspace, args.team.as_deref());
1441
+ Ok(CmdResult::from_json(value, args.json))
1434
1442
  }
1435
1443
 
1436
1444
  /// `cmd_stop_agent`(`commands.py:359`)。
@@ -1457,17 +1465,24 @@ pub fn cmd_reset_agent(args: &ResetAgentArgs) -> Result<CmdResult, CliError> {
1457
1465
 
1458
1466
  /// `cmd_add_agent`(`commands.py:373`)。
1459
1467
  pub fn cmd_add_agent(args: &AddAgentArgs) -> Result<CmdResult, CliError> {
1460
- Ok(CmdResult::from_json(
1461
- lifecycle_port::add_agent(
1462
- &args.workspace,
1463
- &args.agent,
1464
- &args.role_file,
1465
- !args.no_display,
1466
- args.team.as_deref(),
1467
- args.force,
1468
- )?,
1469
- args.json,
1470
- ))
1468
+ let mut value = lifecycle_port::add_agent(
1469
+ &args.workspace,
1470
+ &args.agent,
1471
+ &args.role_file,
1472
+ !args.no_display,
1473
+ args.team.as_deref(),
1474
+ args.force,
1475
+ )?;
1476
+ if value.get("agent_ids").is_none() {
1477
+ if let Some(object) = value.as_object_mut() {
1478
+ object.insert(
1479
+ "agent_ids".to_string(),
1480
+ json!([args.agent.as_str()]),
1481
+ );
1482
+ }
1483
+ }
1484
+ append_send_guidance(&mut value, &args.workspace, args.team.as_deref());
1485
+ Ok(CmdResult::from_json(value, args.json))
1471
1486
  }
1472
1487
 
1473
1488
  /// `cmd_fork_agent`(`commands.py:383`)。
@@ -1613,8 +1628,10 @@ pub fn cmd_doctor(args: &DoctorArgs) -> Result<CmdResult, CliError> {
1613
1628
  mod tests {
1614
1629
  #![allow(clippy::unwrap_used)]
1615
1630
 
1616
- use super::{agent_pane_id, append_send_guidance, quickstart_human, send_command};
1617
- use serde_json::json;
1631
+ use super::{
1632
+ agent_pane_id, append_send_guidance, quickstart_human, send_command, split_shell_argv,
1633
+ };
1634
+ use serde_json::{json, Value};
1618
1635
  use std::path::{Path, PathBuf};
1619
1636
 
1620
1637
  // E13:happy 人类输出必须带 attach 块(此前 else 分支只打 summary 丢 attach_commands)。
@@ -1658,13 +1675,37 @@ mod tests {
1658
1675
  );
1659
1676
  }
1660
1677
 
1678
+ #[test]
1679
+ fn send_command_message_is_one_argv_token() {
1680
+ let command = send_command(
1681
+ "worker name; echo unsafe",
1682
+ Path::new("/tmp/my workspace"),
1683
+ Some("team-a"),
1684
+ )
1685
+ .unwrap();
1686
+ let argv = split_shell_argv(&command);
1687
+ assert_eq!(
1688
+ argv,
1689
+ [
1690
+ "team-agent",
1691
+ "send",
1692
+ "worker name; echo unsafe",
1693
+ "MESSAGE",
1694
+ "--workspace",
1695
+ "/tmp/my workspace",
1696
+ "--team",
1697
+ "team-a"
1698
+ ]
1699
+ );
1700
+ }
1701
+
1661
1702
  #[test]
1662
1703
  fn send_guidance_is_copyable_and_preserves_explicit_scope() {
1663
1704
  let command = send_command("worker name; echo unsafe", Path::new("/tmp/my workspace"), Some("team-a"))
1664
1705
  .unwrap();
1665
1706
  assert_eq!(
1666
1707
  command,
1667
- "team-agent send 'worker name; echo unsafe' \\\"MESSAGE\\\" --workspace '/tmp/my workspace' --team team-a"
1708
+ "team-agent send 'worker name; echo unsafe' MESSAGE --workspace '/tmp/my workspace' --team team-a"
1668
1709
  );
1669
1710
  }
1670
1711
 
@@ -1698,6 +1739,24 @@ mod tests {
1698
1739
  assert!(value.get("send_commands").is_none());
1699
1740
  }
1700
1741
 
1742
+ #[test]
1743
+ fn existing_runtime_gets_canonical_send_guidance() {
1744
+ let mut value = json!({
1745
+ "ok": false,
1746
+ "summary": "existing runtime",
1747
+ "agent_ids": ["worker"]
1748
+ });
1749
+ append_send_guidance(&mut value, Path::new("/tmp/ws"), Some("team-a"));
1750
+ let command = value
1751
+ .pointer("/send_commands/0")
1752
+ .and_then(Value::as_str)
1753
+ .unwrap();
1754
+ assert!(command.contains("send worker"));
1755
+ assert!(command.contains("--team team-a"));
1756
+ let parts: Vec<&str> = command.split_whitespace().collect();
1757
+ assert_eq!(parts[3], "MESSAGE");
1758
+ }
1759
+
1701
1760
  #[test]
1702
1761
  fn agent_pane_id_resolves_session_window_even_with_recorded_pane_id() {
1703
1762
  let state = json!({