@team-agent/installer 0.3.1 → 0.3.3

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 (79) hide show
  1. package/Cargo.lock +34 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +1 -1
  4. package/crates/team-agent/src/cli/adapters.rs +234 -26
  5. package/crates/team-agent/src/cli/diagnose.rs +144 -10
  6. package/crates/team-agent/src/cli/emit.rs +289 -54
  7. package/crates/team-agent/src/cli/leader.rs +37 -8
  8. package/crates/team-agent/src/cli/mod.rs +1281 -196
  9. package/crates/team-agent/src/cli/status_port.rs +195 -46
  10. package/crates/team-agent/src/cli/tests/divergence.rs +1 -2
  11. package/crates/team-agent/src/cli/tests/lane_c.rs +23 -13
  12. package/crates/team-agent/src/cli/tests/main_preserved.rs +2 -0
  13. package/crates/team-agent/src/cli/tests/run_delegation.rs +59 -3
  14. package/crates/team-agent/src/cli/types.rs +18 -0
  15. package/crates/team-agent/src/compiler.rs +15 -5
  16. package/crates/team-agent/src/coordinator/health.rs +95 -17
  17. package/crates/team-agent/src/coordinator/mod.rs +4 -0
  18. package/crates/team-agent/src/coordinator/runtime_detectors.rs +500 -0
  19. package/crates/team-agent/src/coordinator/runtime_observation.rs +58 -0
  20. package/crates/team-agent/src/coordinator/tick.rs +222 -69
  21. package/crates/team-agent/src/coordinator/types.rs +15 -3
  22. package/crates/team-agent/src/db/schema.rs +37 -2
  23. package/crates/team-agent/src/diagnose/comms.rs +226 -0
  24. package/crates/team-agent/src/diagnose/mod.rs +45 -0
  25. package/crates/team-agent/src/diagnose/orphans.rs +658 -0
  26. package/crates/team-agent/src/fake_worker.rs +146 -3
  27. package/crates/team-agent/src/leader/start.rs +121 -23
  28. package/crates/team-agent/src/leader/types.rs +44 -1
  29. package/crates/team-agent/src/lib.rs +3 -0
  30. package/crates/team-agent/src/lifecycle/display.rs +645 -47
  31. package/crates/team-agent/src/lifecycle/launch.rs +1061 -146
  32. package/crates/team-agent/src/lifecycle/mod.rs +2 -0
  33. package/crates/team-agent/src/lifecycle/profile_launch.rs +810 -0
  34. package/crates/team-agent/src/lifecycle/profile_smoke.rs +522 -0
  35. package/crates/team-agent/src/lifecycle/restart/agent.rs +99 -23
  36. package/crates/team-agent/src/lifecycle/restart/common.rs +183 -24
  37. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +498 -22
  38. package/crates/team-agent/src/lifecycle/restart/remove.rs +27 -7
  39. package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
  40. package/crates/team-agent/src/lifecycle/restart.rs +24 -1
  41. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +5 -5
  42. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +37 -7
  43. package/crates/team-agent/src/lifecycle/types.rs +19 -0
  44. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  45. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +341 -0
  46. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +10 -0
  47. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +158 -0
  48. package/crates/team-agent/src/mcp_server/mod.rs +3 -74
  49. package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -1
  50. package/crates/team-agent/src/mcp_server/tests/send.rs +6 -5
  51. package/crates/team-agent/src/mcp_server/tools.rs +312 -111
  52. package/crates/team-agent/src/mcp_server/types.rs +6 -4
  53. package/crates/team-agent/src/mcp_server/wire.rs +19 -7
  54. package/crates/team-agent/src/message_store.rs +21 -4
  55. package/crates/team-agent/src/messaging/delivery.rs +470 -59
  56. package/crates/team-agent/src/messaging/mod.rs +9 -6
  57. package/crates/team-agent/src/messaging/results.rs +353 -63
  58. package/crates/team-agent/src/messaging/selftest.rs +199 -12
  59. package/crates/team-agent/src/messaging/send.rs +35 -3
  60. package/crates/team-agent/src/messaging/tests/runtime.rs +19 -4
  61. package/crates/team-agent/src/messaging/types.rs +11 -3
  62. package/crates/team-agent/src/os_probe.rs +119 -0
  63. package/crates/team-agent/src/packaging/migrate.rs +10 -2
  64. package/crates/team-agent/src/packaging/tests.rs +23 -0
  65. package/crates/team-agent/src/provider/adapter.rs +564 -63
  66. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +1 -7
  67. package/crates/team-agent/src/provider/classify.rs +51 -4
  68. package/crates/team-agent/src/provider/helpers.rs +10 -1
  69. package/crates/team-agent/src/provider/startup_prompt.rs +94 -0
  70. package/crates/team-agent/src/provider/types.rs +47 -0
  71. package/crates/team-agent/src/session_capture.rs +616 -0
  72. package/crates/team-agent/src/state/persist.rs +170 -1
  73. package/crates/team-agent/src/state/projection.rs +141 -8
  74. package/crates/team-agent/src/state/selector.rs +5 -2
  75. package/crates/team-agent/src/tmux_backend.rs +161 -64
  76. package/crates/team-agent/src/transport/test_support.rs +9 -0
  77. package/crates/team-agent/src/transport/tests/wire.rs +4 -0
  78. package/crates/team-agent/src/transport.rs +13 -2
  79. package/package.json +4 -4
package/Cargo.lock CHANGED
@@ -153,6 +153,16 @@ version = "0.1.9"
153
153
  source = "registry+https://github.com/rust-lang/crates.io-index"
154
154
  checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
155
155
 
156
+ [[package]]
157
+ name = "fslock"
158
+ version = "0.2.1"
159
+ source = "registry+https://github.com/rust-lang/crates.io-index"
160
+ checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb"
161
+ dependencies = [
162
+ "libc",
163
+ "winapi",
164
+ ]
165
+
156
166
  [[package]]
157
167
  name = "futures-core"
158
168
  version = "0.3.32"
@@ -494,6 +504,7 @@ version = "3.5.0"
494
504
  source = "registry+https://github.com/rust-lang/crates.io-index"
495
505
  checksum = "699f4197115b8a7e7ff19c9a315a4bd6fffec26cc4626ef45ecaea389e081c6d"
496
506
  dependencies = [
507
+ "fslock",
497
508
  "futures-executor",
498
509
  "futures-util",
499
510
  "log",
@@ -555,7 +566,7 @@ dependencies = [
555
566
 
556
567
  [[package]]
557
568
  name = "team-agent"
558
- version = "0.3.1"
569
+ version = "0.3.3"
559
570
  dependencies = [
560
571
  "anyhow",
561
572
  "chrono",
@@ -658,6 +669,28 @@ dependencies = [
658
669
  "unicode-ident",
659
670
  ]
660
671
 
672
+ [[package]]
673
+ name = "winapi"
674
+ version = "0.3.9"
675
+ source = "registry+https://github.com/rust-lang/crates.io-index"
676
+ checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
677
+ dependencies = [
678
+ "winapi-i686-pc-windows-gnu",
679
+ "winapi-x86_64-pc-windows-gnu",
680
+ ]
681
+
682
+ [[package]]
683
+ name = "winapi-i686-pc-windows-gnu"
684
+ version = "0.4.0"
685
+ source = "registry+https://github.com/rust-lang/crates.io-index"
686
+ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
687
+
688
+ [[package]]
689
+ name = "winapi-x86_64-pc-windows-gnu"
690
+ version = "0.4.0"
691
+ source = "registry+https://github.com/rust-lang/crates.io-index"
692
+ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
693
+
661
694
  [[package]]
662
695
  name = "windows-core"
663
696
  version = "0.62.2"
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent"]
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.3.1"
12
+ version = "0.3.3"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -27,7 +27,7 @@ libc.workspace = true
27
27
  rusqlite.workspace = true # step 3 db(bundled SQLite,静态链接)
28
28
 
29
29
  [dev-dependencies]
30
- serial_test = "3"
30
+ serial_test = { version = "3", features = ["file_locks"] }
31
31
 
32
32
  [lints]
33
33
  workspace = true
@@ -44,6 +44,9 @@ pub fn cmd_init(args: &InitArgs) -> Result<CmdResult, CliError> {
44
44
  let team_root = args.workspace.join(".team");
45
45
  let spec_path = team_root.join("current").join("team.spec.yaml");
46
46
  let state_path = args.workspace.join("team_state.md");
47
+ let team_md_path = args.workspace.join("TEAM.md");
48
+ let agents_dir = args.workspace.join("agents");
49
+ let default_agent_path = agents_dir.join("worker.md");
47
50
  if spec_path.exists() && !args.force {
48
51
  return Err(CliError::Runtime(format!(
49
52
  "{} already exists; pass --force to overwrite",
@@ -57,10 +60,23 @@ pub fn cmd_init(args: &InitArgs) -> Result<CmdResult, CliError> {
57
60
  team_root.join("logs"),
58
61
  team_root.join("messages"),
59
62
  team_root.join("artifacts"),
63
+ agents_dir.clone(),
60
64
  ] {
61
65
  std::fs::create_dir_all(&dir)?;
62
66
  }
63
67
  std::fs::write(&spec_path, INIT_SPEC_TEMPLATE)?;
68
+ if args.force || !team_md_path.exists() {
69
+ std::fs::write(
70
+ &team_md_path,
71
+ "---\nname: current\nobjective: Pending.\nprovider: fake\n---\n\nPending.\n",
72
+ )?;
73
+ }
74
+ if args.force || !default_agent_path.exists() {
75
+ std::fs::write(
76
+ &default_agent_path,
77
+ "---\nname: worker\nrole: Worker\nprovider: fake\ntools:\n - mcp_team\n---\n\nWait for instructions.\n",
78
+ )?;
79
+ }
64
80
  if args.force || !state_path.exists() {
65
81
  std::fs::write(&state_path, INIT_STATE_TEMPLATE)?;
66
82
  }
@@ -85,15 +101,37 @@ pub fn cmd_init(args: &InitArgs) -> Result<CmdResult, CliError> {
85
101
  /// `cmd_quick_start`(`commands.py:18`)。`--json` 或 `!ok` → 整 dict;否则 `result["summary"]`。
86
102
  pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
87
103
  let value = lifecycle_port::quick_start(
88
- &args.agents_dir,
104
+ &args.workspace,
89
105
  &args.agents_dir,
90
106
  args.name.as_deref(),
91
107
  args.team_id.as_deref(),
92
108
  args.yes,
93
109
  args.fresh,
94
110
  )?;
95
- if args.json || value.get("ok").and_then(Value::as_bool) == Some(false) {
96
- Ok(CmdResult::from_json(value, args.json))
111
+ let readiness = value.get("readiness").and_then(Value::as_object);
112
+ let all_resumable_have_session = readiness
113
+ .and_then(|readiness| readiness.get("all_resumable_have_session"))
114
+ .and_then(Value::as_bool)
115
+ .unwrap_or(true);
116
+ let session_capture_incomplete = readiness
117
+ .and_then(|readiness| readiness.get("session_capture_incomplete"))
118
+ .and_then(Value::as_bool)
119
+ .unwrap_or(!all_resumable_have_session);
120
+ let readiness_ready = readiness
121
+ .and_then(|readiness| readiness.get("ready"))
122
+ .and_then(Value::as_bool)
123
+ .unwrap_or(true);
124
+ let status = value.get("status").and_then(Value::as_str).map(str::to_string);
125
+ if args.json
126
+ || value.get("ok").and_then(Value::as_bool) == Some(false)
127
+ || session_capture_incomplete
128
+ || !readiness_ready
129
+ {
130
+ let mut result = CmdResult::from_json(value, args.json);
131
+ if args.json && status.as_deref() == Some("pending_tool_load") {
132
+ result.exit = ExitCode::Ok;
133
+ }
134
+ Ok(result)
97
135
  } else {
98
136
  Ok(CmdResult::human(
99
137
  value
@@ -123,6 +161,10 @@ pub fn cmd_compile(args: &CompileArgs) -> Result<CmdResult, CliError> {
123
161
  /// `cmd_status`(`commands.py:90`)。三态:`--summary`(xor json,xor agent)→五行文本;
124
162
  /// `--json`→`status_port::status(compact=!detail)`;else→`status_port::format_status(agent)`。
125
163
  pub fn cmd_status(args: &StatusArgs) -> Result<CmdResult, CliError> {
164
+ cmd_status_for_team(args, None)
165
+ }
166
+
167
+ pub fn cmd_status_for_team(args: &StatusArgs, team: Option<&str>) -> Result<CmdResult, CliError> {
126
168
  if args.summary && args.json {
127
169
  return Err(CliError::Runtime(
128
170
  "--summary and --json are mutually exclusive".to_string(),
@@ -135,7 +177,7 @@ pub fn cmd_status(args: &StatusArgs) -> Result<CmdResult, CliError> {
135
177
  }
136
178
  let selected = match crate::state::selector::resolve_active_team(
137
179
  &args.workspace,
138
- None,
180
+ team,
139
181
  crate::state::selector::SelectorMode::RuntimeOnly,
140
182
  ) {
141
183
  Ok(selected) => selected,
@@ -147,15 +189,29 @@ pub fn cmd_status(args: &StatusArgs) -> Result<CmdResult, CliError> {
147
189
  }
148
190
  };
149
191
  if args.summary {
150
- let value = status_port::status(&selected.run_workspace, true, false)?;
192
+ let value = status_port::status_scoped(
193
+ &selected.run_workspace,
194
+ &selected.state,
195
+ Some(&selected.team_key),
196
+ true,
197
+ false,
198
+ )?;
151
199
  return Ok(CmdResult::human(format_status_summary(&value)));
152
200
  }
153
201
  if args.json {
154
- let value = status_port::status(&selected.run_workspace, status_compact_flag(args.detail), args.detail)?;
202
+ let value = status_port::status_scoped(
203
+ &selected.run_workspace,
204
+ &selected.state,
205
+ Some(&selected.team_key),
206
+ status_compact_flag(args.detail),
207
+ args.detail,
208
+ )?;
155
209
  return Ok(CmdResult::from_json(value, true));
156
210
  }
157
- Ok(CmdResult::human(status_port::format_status(
211
+ Ok(CmdResult::human(status_port::format_status_scoped(
158
212
  &selected.run_workspace,
213
+ &selected.state,
214
+ Some(&selected.team_key),
159
215
  args.agent.as_deref(),
160
216
  )?))
161
217
  }
@@ -252,9 +308,13 @@ pub fn cmd_validate_result(args: &ValidateResultArgs) -> Result<CmdResult, CliEr
252
308
 
253
309
  /// `cmd_collect`(`parser.py:292`)。
254
310
  pub fn cmd_collect(args: &CollectArgs) -> Result<CmdResult, CliError> {
311
+ cmd_collect_for_team(args, None)
312
+ }
313
+
314
+ pub fn cmd_collect_for_team(args: &CollectArgs, team: Option<&str>) -> Result<CmdResult, CliError> {
255
315
  let selected = match crate::state::selector::resolve_active_team(
256
316
  &args.workspace,
257
- None,
317
+ team,
258
318
  crate::state::selector::SelectorMode::RuntimeOnly,
259
319
  ) {
260
320
  Ok(selected) => selected,
@@ -269,7 +329,16 @@ pub fn cmd_collect(args: &CollectArgs) -> Result<CmdResult, CliError> {
269
329
  ));
270
330
  }
271
331
  };
272
- let value = match messaging::collect(&selected.run_workspace, args.result_file.as_deref(), false) {
332
+ let value = match if team.is_some() {
333
+ messaging::collect_for_team(
334
+ &selected.run_workspace,
335
+ args.result_file.as_deref(),
336
+ false,
337
+ Some(&selected.team_key),
338
+ )
339
+ } else {
340
+ messaging::collect(&selected.run_workspace, args.result_file.as_deref(), false)
341
+ } {
273
342
  Ok(value) => value,
274
343
  Err(error) => {
275
344
  return Ok(CmdResult::from_json(
@@ -329,7 +398,18 @@ pub fn cmd_settle(args: &SettleArgs) -> Result<CmdResult, CliError> {
329
398
  }
330
399
 
331
400
  fn settle_value(workspace: &Path) -> Result<Value, CliError> {
332
- let mut collect = messaging::collect(workspace, None, false)?;
401
+ let selected = crate::state::selector::resolve_active_team(
402
+ workspace,
403
+ None,
404
+ crate::state::selector::SelectorMode::RuntimeOnly,
405
+ )
406
+ .map_err(|e| CliError::Runtime(e.to_string()))?;
407
+ let mut collect = messaging::collect_for_team(
408
+ &selected.run_workspace,
409
+ None,
410
+ false,
411
+ Some(&selected.team_key),
412
+ )?;
333
413
  if collect.get("ok").and_then(Value::as_bool) == Some(false) {
334
414
  let message = collect
335
415
  .get("error")
@@ -338,7 +418,7 @@ fn settle_value(workspace: &Path) -> Result<Value, CliError> {
338
418
  return Err(CliError::Runtime(message.to_string()));
339
419
  }
340
420
  let coordinator_log = crate::coordinator::coordinator_log_path(
341
- &crate::coordinator::WorkspacePath::new(workspace.to_path_buf()),
421
+ &crate::coordinator::WorkspacePath::new(selected.run_workspace.clone()),
342
422
  );
343
423
  let collect_object = collect
344
424
  .as_object_mut()
@@ -351,21 +431,92 @@ fn settle_value(workspace: &Path) -> Result<Value, CliError> {
351
431
  "log": coordinator_log.to_string_lossy().to_string(),
352
432
  }),
353
433
  );
354
- let status = status_port::status(workspace, true, false)?;
355
- let details_log = write_settle_details_log(workspace, &collect, &status)?;
434
+ collect_object.insert("team_key".to_string(), json!(selected.team_key.clone()));
435
+ collect_object.insert("active_team_key".to_string(), json!(selected.team_key.clone()));
436
+ collect_object.insert("team".to_string(), json!(selected.team_key.clone()));
437
+ if let Some(collected_results) = collect_object.get("collected_results").cloned() {
438
+ collect_object.insert("collected".to_string(), collected_results);
439
+ }
440
+ let status_state =
441
+ crate::state::projection::select_runtime_state(&selected.run_workspace, Some(&selected.team_key))?;
442
+ let state_file = match (selected.spec_path.as_ref(), selected.spec_workspace.as_ref()) {
443
+ (Some(spec_path), Some(spec_workspace)) => match load_team_spec_at(spec_path)? {
444
+ Some(spec) => crate::lifecycle::restart::write_team_state(spec_workspace, &spec, &status_state)
445
+ .map_err(|e| CliError::Runtime(e.to_string()))?
446
+ .to_string_lossy()
447
+ .to_string(),
448
+ None => collect
449
+ .get("state_file")
450
+ .and_then(Value::as_str)
451
+ .unwrap_or("")
452
+ .to_string(),
453
+ },
454
+ _ => collect
455
+ .get("state_file")
456
+ .and_then(Value::as_str)
457
+ .unwrap_or("")
458
+ .to_string(),
459
+ };
460
+ if let Some(obj) = collect.as_object_mut() {
461
+ obj.insert("state_file".to_string(), json!(state_file.clone()));
462
+ }
463
+ let mut status = status_port::status_scoped(
464
+ &selected.run_workspace,
465
+ &status_state,
466
+ Some(&selected.team_key),
467
+ true,
468
+ false,
469
+ )?;
470
+ if let Some(obj) = status.as_object_mut() {
471
+ obj.insert("team_key".to_string(), json!(selected.team_key.clone()));
472
+ obj.insert("active_team_key".to_string(), json!(selected.team_key.clone()));
473
+ obj.insert("team".to_string(), json!(selected.team_key.clone()));
474
+ }
475
+ let details_log = write_settle_details_log(&selected.run_workspace, &collect, &status)?;
356
476
  let collected_count = collect
357
477
  .get("collected")
358
478
  .and_then(Value::as_array)
359
479
  .map_or(0, Vec::len);
480
+ let settled_results = settle_collected_results_for_team(
481
+ collect.get("collected_results"),
482
+ &selected.team_key,
483
+ );
360
484
  Ok(json!({
361
485
  "ok": true,
362
486
  "summary": format!("collected {collected_count} result(s)"),
363
487
  "next_actions": ["Review team_state.md and decide whether to continue or shutdown."],
364
488
  "details_log": details_log.to_string_lossy().to_string(),
489
+ "collected_results": settled_results,
490
+ "collected": collect.get("collected").cloned().unwrap_or_else(|| json!([])),
491
+ "results": collect.get("results").cloned().unwrap_or_else(|| json!({})),
492
+ "state_file": state_file,
493
+ "status": status,
365
494
  "collect": collect,
495
+ "team_key": selected.team_key,
496
+ "active_team_key": selected.team_key,
497
+ "team": selected.team_key,
498
+ "workspace": selected.run_workspace.to_string_lossy().to_string(),
366
499
  }))
367
500
  }
368
501
 
502
+ fn settle_collected_results_for_team(value: Option<&Value>, team_key: &str) -> Value {
503
+ let Some(Value::Array(results)) = value else {
504
+ return json!([]);
505
+ };
506
+ Value::Array(
507
+ results
508
+ .iter()
509
+ .map(|result| {
510
+ let mut result = result.clone();
511
+ if let Some(obj) = result.as_object_mut() {
512
+ obj.insert("owner_team_id".to_string(), json!(team_key));
513
+ }
514
+ result
515
+ })
516
+ .collect(),
517
+ )
518
+ }
519
+
369
520
  fn write_settle_details_log(workspace: &Path, collect: &Value, status: &Value) -> Result<PathBuf, CliError> {
370
521
  let logs = workspace.join(".team").join("logs");
371
522
  std::fs::create_dir_all(&logs)?;
@@ -396,7 +547,13 @@ pub fn cmd_repair_state(args: &RepairStateArgs) -> Result<CmdResult, CliError> {
396
547
  args.status
397
548
  )));
398
549
  }
399
- let mut state = crate::state::persist::load_runtime_state(&args.workspace)?;
550
+ let selected = crate::state::selector::resolve_active_team(
551
+ &args.workspace,
552
+ None,
553
+ crate::state::selector::SelectorMode::RequireSpec,
554
+ )
555
+ .map_err(|e| CliError::Runtime(e.to_string()))?;
556
+ let mut state = selected.state;
400
557
  let before = find_task_projection(&state, &args.task_id).unwrap_or_else(repair_task_projection_null);
401
558
  update_task(
402
559
  &mut state,
@@ -406,12 +563,20 @@ pub fn cmd_repair_state(args: &RepairStateArgs) -> Result<CmdResult, CliError> {
406
563
  args.summary.as_deref(),
407
564
  );
408
565
  let after = find_task_projection(&state, &args.task_id).unwrap_or_else(repair_task_projection_null);
409
- crate::state::persist::save_runtime_state(&args.workspace, &state)?;
410
- let spec = load_team_spec_optional(&args.workspace, &state)?
566
+ crate::state::projection::save_team_scoped_state(&selected.run_workspace, &state)?;
567
+ let spec_path = selected
568
+ .spec_path
569
+ .as_ref()
570
+ .ok_or_else(|| CliError::Runtime("team.spec.yaml not found".to_string()))?;
571
+ let spec = load_team_spec_at(spec_path)?
411
572
  .ok_or_else(|| CliError::Runtime("team.spec.yaml not found".to_string()))?;
412
- let state_file = crate::lifecycle::restart::write_team_state(&args.workspace, &spec, &state)
573
+ let spec_workspace = selected
574
+ .spec_workspace
575
+ .as_ref()
576
+ .ok_or_else(|| CliError::Runtime("active team spec workspace not found".to_string()))?;
577
+ let state_file = crate::lifecycle::restart::write_team_state(spec_workspace, &spec, &state)
413
578
  .map_err(|e| CliError::Runtime(e.to_string()))?;
414
- crate::event_log::EventLog::new(&args.workspace)
579
+ crate::event_log::EventLog::new(&selected.run_workspace)
415
580
  .write(
416
581
  "repair_state.task",
417
582
  json!({
@@ -435,9 +600,15 @@ pub fn cmd_repair_state(args: &RepairStateArgs) -> Result<CmdResult, CliError> {
435
600
 
436
601
  /// `cmd_diagnose`(`parser.py:298`)。
437
602
  pub fn cmd_diagnose(args: &DiagnoseArgs) -> Result<CmdResult, CliError> {
438
- let state = crate::state::persist::load_runtime_state(&args.workspace)?;
439
- let event_log = args.workspace.join(".team").join("logs").join("events.jsonl");
440
- let backend = crate::tmux_backend::TmuxBackend::for_workspace(&args.workspace);
603
+ let selected = crate::state::selector::resolve_active_team(
604
+ &args.workspace,
605
+ None,
606
+ crate::state::selector::SelectorMode::RuntimeOnly,
607
+ )
608
+ .map_err(|e| CliError::Runtime(e.to_string()))?;
609
+ let state = selected.state;
610
+ let event_log = selected.run_workspace.join(".team").join("logs").join("events.jsonl");
611
+ let backend = crate::tmux_backend::TmuxBackend::for_workspace(&selected.run_workspace);
441
612
  let (issues, suggested_repairs) = diagnose_runtime(&state, &backend);
442
613
  let ok = issues.as_array().is_some_and(Vec::is_empty);
443
614
  Ok(CmdResult::from_json(
@@ -448,6 +619,7 @@ pub fn cmd_diagnose(args: &DiagnoseArgs) -> Result<CmdResult, CliError> {
448
619
  "providers": provider_doctor_checks(),
449
620
  "runtime": {
450
621
  "workspace": args.workspace.to_string_lossy().to_string(),
622
+ "team_key": selected.team_key,
451
623
  "session_name": state.get("session_name").cloned().unwrap_or(Value::Null),
452
624
  "leader_receiver": state.get("leader_receiver").cloned().unwrap_or(Value::Null),
453
625
  "agent_count": state.get("agents").and_then(Value::as_object).map_or(0, serde_json::Map::len),
@@ -528,18 +700,33 @@ pub fn cmd_peek(args: &PeekArgs) -> Result<CmdResult, CliError> {
528
700
  if !windows.iter().any(|w| w.as_str() == window) {
529
701
  return Ok(peek_unavailable(&args.agent, args.json));
530
702
  }
703
+ let range = args
704
+ .head
705
+ .map(|head| crate::transport::CaptureRange::Head(head as u32))
706
+ .unwrap_or_else(|| crate::transport::CaptureRange::Tail(args.tail as u32));
531
707
  let capture = backend
532
708
  .capture(
533
709
  &crate::transport::Target::Pane(crate::transport::PaneId::new(target.clone())),
534
- crate::transport::CaptureRange::Tail(args.tail as u32),
710
+ range,
535
711
  )
536
712
  .map_err(|e| CliError::Runtime(e.to_string()))?;
713
+ let matches = args.search.as_ref().map(|needle| {
714
+ capture
715
+ .text
716
+ .lines()
717
+ .filter(|line| line.contains(needle))
718
+ .map(str::to_string)
719
+ .collect::<Vec<_>>()
720
+ });
537
721
  Ok(CmdResult::from_json(
538
722
  json!({
539
723
  "ok": true,
540
724
  "agent_id": args.agent,
541
725
  "workspace": args.workspace.to_string_lossy().to_string(),
542
726
  "tail": args.tail,
727
+ "head": args.head,
728
+ "search": args.search,
729
+ "matches": matches,
543
730
  "pane_id": target,
544
731
  "text": capture.text,
545
732
  }),
@@ -603,6 +790,12 @@ fn run_fake_e2e(workspace: &Path) -> Result<Value, CliError> {
603
790
  "reason": send.reason,
604
791
  });
605
792
  if send.ok {
793
+ if let Some(message_id) = send.message_id.as_deref() {
794
+ crate::message_store::MessageStore::open(workspace)
795
+ .map_err(crate::messaging::MessagingError::from)?
796
+ .mark(message_id, "delivered", None)
797
+ .map_err(crate::messaging::MessagingError::from)?;
798
+ }
606
799
  let _ = messaging::report_result(
607
800
  workspace,
608
801
  &json!({
@@ -1052,6 +1245,16 @@ fn load_team_spec_optional(workspace: &Path, state: &Value) -> Result<Option<cra
1052
1245
  .map_err(|e| CliError::Runtime(e.to_string()))
1053
1246
  }
1054
1247
 
1248
+ fn load_team_spec_at(spec_path: &Path) -> Result<Option<crate::model::yaml::Value>, CliError> {
1249
+ if !spec_path.exists() {
1250
+ return Ok(None);
1251
+ }
1252
+ let text = std::fs::read_to_string(spec_path)?;
1253
+ crate::model::yaml::loads(&text)
1254
+ .map(Some)
1255
+ .map_err(|e| CliError::Runtime(e.to_string()))
1256
+ }
1257
+
1055
1258
  /// `cmd_approvals`(`commands.py:112`)。
1056
1259
  pub fn cmd_approvals(args: &ApprovalsArgs) -> Result<CmdResult, CliError> {
1057
1260
  let value = status_port::approvals(&args.workspace, args.agent.as_deref(), args.json)?;
@@ -1167,7 +1370,12 @@ pub fn cmd_shutdown(args: &ShutdownArgs) -> Result<CmdResult, CliError> {
1167
1370
  /// `cmd_restart`(`commands.py:344`)。
1168
1371
  pub fn cmd_restart(args: &RestartArgs) -> Result<CmdResult, CliError> {
1169
1372
  Ok(CmdResult::from_json(
1170
- lifecycle_port::restart(&args.workspace, args.allow_fresh, args.team.as_deref())?,
1373
+ lifecycle_port::restart(
1374
+ &args.workspace,
1375
+ args.allow_fresh,
1376
+ args.team.as_deref(),
1377
+ args.session_converge_deadline_ms,
1378
+ )?,
1171
1379
  args.json,
1172
1380
  ))
1173
1381
  }
@@ -1283,7 +1491,7 @@ pub fn cmd_doctor(args: &DoctorArgs) -> Result<CmdResult, CliError> {
1283
1491
  return Err(CliError::Runtime("--fix requires --gate".to_string()));
1284
1492
  }
1285
1493
  if args.comms || matches!(args.gate, Some(DoctorGate::Comms)) {
1286
- let value = diagnose_port::comms_selftest(&args.workspace, args.team.as_deref(), Some("comms"))?;
1494
+ let value = crate::diagnose::comms::doctor_comms_json(&args.workspace, args.team.as_deref(), Some("comms"))?;
1287
1495
  if !args.json {
1288
1496
  let json_tail = serde_json::to_string_pretty(&sort_json(&value))?;
1289
1497
  return Ok(CmdResult::human(format!("{COMMS_BOUNDARY_TEXT}\n{json_tail}")));
@@ -1291,9 +1499,9 @@ pub fn cmd_doctor(args: &DoctorArgs) -> Result<CmdResult, CliError> {
1291
1499
  return Ok(CmdResult::from_json(value, true));
1292
1500
  }
1293
1501
  let value = if matches!(args.gate, Some(DoctorGate::Orphans)) {
1294
- diagnose_port::orphan_gate(args.fix, args.confirm)?
1502
+ crate::diagnose::orphans::orphan_gate_json(args.fix, args.confirm)?
1295
1503
  } else if args.cleanup_orphans {
1296
- diagnose_port::cleanup_orphans(args.confirm)?
1504
+ crate::diagnose::orphans::cleanup_orphans_json(args.confirm)?
1297
1505
  } else if args.fix_schema {
1298
1506
  diagnose_port::fix_schema(&args.workspace)?
1299
1507
  } else {