@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
@@ -1,11 +1,25 @@
1
1
  //! status_port extracted from cli::mod inline placeholder.
2
2
  use super::*;
3
+ use crate::state::projection::OwnerTeamResolution;
3
4
  use crate::transport::Transport;
5
+ use rusqlite::params;
4
6
 
5
7
  /// `status.status(workspace, as_json, compact)`(`queries.py:33`,**有副作用**:capture→refresh→save)。
6
8
  pub fn status(workspace: &Path, compact: bool, detail: bool) -> Result<Value, CliError> {
7
- let _ = detail;
8
9
  let state = read_runtime_state(workspace);
10
+ status_scoped(workspace, &state, None, compact, detail)
11
+ }
12
+
13
+ pub fn status_scoped(
14
+ workspace: &Path,
15
+ state: &Value,
16
+ owner_team_id: Option<&str>,
17
+ compact: bool,
18
+ detail: bool,
19
+ ) -> Result<Value, CliError> {
20
+ let _ = detail;
21
+ let resolved_owner_team_id = resolve_status_owner_team(workspace, owner_team_id)?;
22
+ let owner_team_id = resolved_owner_team_id.as_deref().or(owner_team_id);
9
23
  let health = crate::coordinator::coordinator_health(
10
24
  &crate::coordinator::WorkspacePath::new(workspace.to_path_buf()),
11
25
  );
@@ -23,19 +37,34 @@ use crate::transport::Transport;
23
37
  .cloned()
24
38
  .unwrap_or_else(|| json!({}));
25
39
  let session_name = state.get("session_name").cloned().unwrap_or(Value::Null);
40
+ let tmux_present = tmux_session_present(workspace, session_name.as_str());
41
+ let mut readiness_state = state.clone();
42
+ if let Some(obj) = readiness_state.as_object_mut() {
43
+ obj.insert("tmux_session_present".to_string(), serde_json::json!(tmux_present));
44
+ }
45
+ let readiness = crate::cli::diagnose::wait_readiness(&readiness_state);
26
46
  let full = json!({
47
+ "ok": true,
27
48
  "team": state.pointer("/leader/id").cloned().unwrap_or_else(|| json!("leader")),
28
49
  "session_name": state.get("session_name").cloned().unwrap_or(Value::Null),
29
- "tmux_session_present": tmux_session_present(workspace, session_name.as_str()),
50
+ "tmux_session_present": tmux_present,
51
+ "all_spawned": readiness.get("all_spawned").cloned().unwrap_or(Value::Bool(false)),
52
+ "all_attached_receiver": readiness.get("all_attached_receiver").cloned().unwrap_or(Value::Bool(true)),
53
+ "all_resumable_have_session": readiness.get("all_resumable_have_session").cloned().unwrap_or(Value::Bool(true)),
54
+ "session_capture_complete": readiness.get("session_capture_complete").cloned().unwrap_or(Value::Bool(true)),
55
+ "session_capture_incomplete": readiness.get("session_capture_incomplete").cloned().unwrap_or(Value::Bool(false)),
56
+ "incomplete_session_capture_agents": readiness.get("incomplete_session_capture_agents").cloned().unwrap_or_else(|| json!([])),
57
+ "pending_session_agent_ids": readiness.get("pending_session_agent_ids").cloned().unwrap_or_else(|| json!([])),
30
58
  "leader_receiver": leader_receiver,
31
59
  "teams": state.get("teams").cloned().unwrap_or_else(|| json!({})),
32
60
  "agents": agents,
33
- "agent_health": agent_health(&conn)?,
61
+ "agent_health": agent_health(&conn, owner_team_id)?,
34
62
  "tasks": tasks,
35
- "messages": message_counts(&conn)?,
36
- "queued_messages": queued_messages(&conn, 8)?,
37
- "results": result_counts(&conn)?,
63
+ "messages": message_counts(&conn, owner_team_id)?,
64
+ "queued_messages": queued_messages(&conn, owner_team_id, 8)?,
65
+ "results": result_counts(&conn, owner_team_id)?,
38
66
  "latest_results": json!([]),
67
+ "readiness": readiness,
39
68
  "coordinator": coordinator_health_value(health),
40
69
  "last_events": Value::Array(
41
70
  crate::event_log::EventLog::new(workspace)
@@ -51,7 +80,17 @@ use crate::transport::Transport;
51
80
  }
52
81
  /// `status.format_status(workspace, agent)`(人读)。
53
82
  pub fn format_status(workspace: &Path, agent: Option<&str>) -> Result<String, CliError> {
54
- let status = status(workspace, true, false)?;
83
+ let state = read_runtime_state(workspace);
84
+ format_status_scoped(workspace, &state, None, agent)
85
+ }
86
+
87
+ pub fn format_status_scoped(
88
+ workspace: &Path,
89
+ state: &Value,
90
+ owner_team_id: Option<&str>,
91
+ agent: Option<&str>,
92
+ ) -> Result<String, CliError> {
93
+ let status = status_scoped(workspace, state, owner_team_id, true, false)?;
55
94
  Ok(match agent {
56
95
  Some(agent) => format!("agent {agent}: {}", status.pointer("/agents").is_some()),
57
96
  None => crate::cli::format_status_summary(&status),
@@ -165,6 +204,32 @@ use crate::transport::Transport;
165
204
  .unwrap_or_else(|| json!({}))
166
205
  }
167
206
 
207
+ fn resolve_status_owner_team(
208
+ workspace: &Path,
209
+ owner_team_id: Option<&str>,
210
+ ) -> Result<Option<String>, CliError> {
211
+ let Some(requested) = owner_team_id.filter(|team| !team.is_empty()) else {
212
+ return Ok(None);
213
+ };
214
+ let state = read_runtime_state(workspace);
215
+ match crate::state::projection::resolve_owner_team_id(&state, requested) {
216
+ OwnerTeamResolution::Canonical(canonical) => Ok(Some(canonical)),
217
+ OwnerTeamResolution::LegacyAlias { requested, canonical } => {
218
+ let log = crate::event_log::EventLog::new(workspace);
219
+ crate::messaging::delivery::normalize_owner_team_id_rows(
220
+ workspace,
221
+ &requested,
222
+ &canonical,
223
+ None,
224
+ Some(&log),
225
+ )
226
+ .map_err(CliError::from)?;
227
+ Ok(Some(canonical))
228
+ }
229
+ OwnerTeamResolution::Unresolved { .. } | OwnerTeamResolution::Ambiguous { .. } => Ok(None),
230
+ }
231
+ }
232
+
168
233
  fn agent_window(agent_id: &str, agent_state: &Value) -> String {
169
234
  ["window", "window_name"]
170
235
  .iter()
@@ -224,15 +289,15 @@ use crate::transport::Transport;
224
289
  .unwrap_or(false)
225
290
  }
226
291
 
227
- fn message_counts(conn: &rusqlite::Connection) -> Result<Value, CliError> {
228
- status_counts(conn, "messages")
292
+ fn message_counts(conn: &rusqlite::Connection, owner_team_id: Option<&str>) -> Result<Value, CliError> {
293
+ status_counts(conn, "messages", owner_team_id)
229
294
  }
230
295
 
231
- fn result_counts(conn: &rusqlite::Connection) -> Result<Value, CliError> {
232
- let by_status = result_status_counts(conn)?;
233
- let total = count_rows(conn, "results")?;
234
- let invalid = count_where_status(conn, "results", "invalid")?;
235
- let collected = count_where_status(conn, "results", "collected")?;
296
+ fn result_counts(conn: &rusqlite::Connection, owner_team_id: Option<&str>) -> Result<Value, CliError> {
297
+ let by_status = result_status_counts(conn, owner_team_id)?;
298
+ let total = count_rows(conn, "results", owner_team_id)?;
299
+ let invalid = count_where_status(conn, "results", owner_team_id, "invalid")?;
300
+ let collected = count_where_status(conn, "results", owner_team_id, "collected")?;
236
301
  let uncollected = total.saturating_sub(collected).saturating_sub(invalid);
237
302
  Ok(json!({
238
303
  "total": total,
@@ -243,10 +308,24 @@ use crate::transport::Transport;
243
308
  }))
244
309
  }
245
310
 
246
- fn status_counts(conn: &rusqlite::Connection, table: &str) -> Result<Value, CliError> {
247
- let sql = format!("select status, count(*) from {table} group by status order by status");
311
+ fn status_counts(
312
+ conn: &rusqlite::Connection,
313
+ table: &str,
314
+ owner_team_id: Option<&str>,
315
+ ) -> Result<Value, CliError> {
316
+ let sql = match owner_team_id {
317
+ Some(_) => format!(
318
+ "select status, count(*) from {table}
319
+ where owner_team_id = ?1
320
+ group by status order by status"
321
+ ),
322
+ None => format!("select status, count(*) from {table} group by status order by status"),
323
+ };
248
324
  let mut stmt = conn.prepare(&sql).map_err(|e| CliError::Runtime(e.to_string()))?;
249
- let mut rows = stmt.query([]).map_err(|e| CliError::Runtime(e.to_string()))?;
325
+ let mut rows = match owner_team_id {
326
+ Some(team) => stmt.query(params![team]).map_err(|e| CliError::Runtime(e.to_string()))?,
327
+ None => stmt.query([]).map_err(|e| CliError::Runtime(e.to_string()))?,
328
+ };
250
329
  let mut out = Map::new();
251
330
  while let Some(row) = rows.next().map_err(|e| CliError::Runtime(e.to_string()))? {
252
331
  let status: String = row.get(0).map_err(|e| CliError::Runtime(e.to_string()))?;
@@ -256,16 +335,28 @@ use crate::transport::Transport;
256
335
  Ok(Value::Object(out))
257
336
  }
258
337
 
259
- fn result_status_counts(conn: &rusqlite::Connection) -> Result<Value, CliError> {
260
- let mut stmt = conn
261
- .prepare(
338
+ fn result_status_counts(conn: &rusqlite::Connection, owner_team_id: Option<&str>) -> Result<Value, CliError> {
339
+ let sql = match owner_team_id {
340
+ Some(_) => {
341
+ "select status, count(*) from results
342
+ where status not in ('collected', 'invalid') and owner_team_id = ?1
343
+ group by status
344
+ order by status"
345
+ }
346
+ None => {
262
347
  "select status, count(*) from results
263
348
  where status not in ('collected', 'invalid')
264
349
  group by status
265
- order by status",
266
- )
350
+ order by status"
351
+ }
352
+ };
353
+ let mut stmt = conn
354
+ .prepare(sql)
267
355
  .map_err(|e| CliError::Runtime(e.to_string()))?;
268
- let mut rows = stmt.query([]).map_err(|e| CliError::Runtime(e.to_string()))?;
356
+ let mut rows = match owner_team_id {
357
+ Some(team) => stmt.query(params![team]).map_err(|e| CliError::Runtime(e.to_string()))?,
358
+ None => stmt.query([]).map_err(|e| CliError::Runtime(e.to_string()))?,
359
+ };
269
360
  let mut out = Map::new();
270
361
  while let Some(row) = rows.next().map_err(|e| CliError::Runtime(e.to_string()))? {
271
362
  let status: String = row.get(0).map_err(|e| CliError::Runtime(e.to_string()))?;
@@ -275,19 +366,32 @@ use crate::transport::Transport;
275
366
  Ok(Value::Object(out))
276
367
  }
277
368
 
278
- fn queued_messages(conn: &rusqlite::Connection, limit: usize) -> Result<Value, CliError> {
369
+ fn queued_messages(
370
+ conn: &rusqlite::Connection,
371
+ owner_team_id: Option<&str>,
372
+ limit: usize,
373
+ ) -> Result<Value, CliError> {
279
374
  let limit = i64::try_from(limit).unwrap_or(i64::MAX);
280
- let mut stmt = conn
281
- .prepare(
375
+ let sql = match owner_team_id {
376
+ Some(_) => {
377
+ "select message_id, recipient, status, created_at, delivery_attempts
378
+ from messages
379
+ where status like 'queued%' and owner_team_id = ?1
380
+ order by created_at desc
381
+ limit ?2"
382
+ }
383
+ None => {
282
384
  "select message_id, recipient, status, created_at, delivery_attempts
283
385
  from messages
284
386
  where status like 'queued%'
285
387
  order by created_at desc
286
- limit ?1",
287
- )
388
+ limit ?1"
389
+ }
390
+ };
391
+ let mut stmt = conn
392
+ .prepare(sql)
288
393
  .map_err(|e| CliError::Runtime(e.to_string()))?;
289
- let rows = stmt
290
- .query_map([limit], |row| {
394
+ let map_row = |row: &rusqlite::Row<'_>| {
291
395
  Ok(json!({
292
396
  "message_id": row.get::<_, String>(0)?,
293
397
  "recipient": row.get::<_, Option<String>>(1)?,
@@ -295,8 +399,12 @@ use crate::transport::Transport;
295
399
  "created_at": row.get::<_, Option<String>>(3)?,
296
400
  "delivery_attempts": row.get::<_, i64>(4)?,
297
401
  }))
298
- })
299
- .map_err(|e| CliError::Runtime(e.to_string()))?;
402
+ };
403
+ let rows = match owner_team_id {
404
+ Some(team) => stmt.query_map(params![team, limit], map_row),
405
+ None => stmt.query_map(params![limit], map_row),
406
+ }
407
+ .map_err(|e| CliError::Runtime(e.to_string()))?;
300
408
  let values = rows
301
409
  .collect::<Result<Vec<_>, _>>()
302
410
  .map_err(|e| CliError::Runtime(e.to_string()))?;
@@ -308,6 +416,13 @@ use crate::transport::Transport;
308
416
  "team": full.get("team").cloned().unwrap_or(Value::Null),
309
417
  "session_name": full.get("session_name").cloned().unwrap_or(Value::Null),
310
418
  "tmux_session_present": full.get("tmux_session_present").cloned().unwrap_or(Value::Bool(false)),
419
+ "all_spawned": full.get("all_spawned").cloned().unwrap_or(Value::Bool(false)),
420
+ "all_attached_receiver": full.get("all_attached_receiver").cloned().unwrap_or(Value::Bool(true)),
421
+ "all_resumable_have_session": full.get("all_resumable_have_session").cloned().unwrap_or(Value::Bool(true)),
422
+ "session_capture_complete": full.get("session_capture_complete").cloned().unwrap_or(Value::Bool(true)),
423
+ "session_capture_incomplete": full.get("session_capture_incomplete").cloned().unwrap_or(Value::Bool(false)),
424
+ "incomplete_session_capture_agents": full.get("incomplete_session_capture_agents").cloned().unwrap_or_else(|| json!([])),
425
+ "pending_session_agent_ids": full.get("pending_session_agent_ids").cloned().unwrap_or_else(|| json!([])),
311
426
  "leader_receiver": compact_object(full.get("leader_receiver"), &[
312
427
  "status", "provider", "mode", "session_name", "window_name", "pane_id", "pane_current_command",
313
428
  ]),
@@ -318,6 +433,7 @@ use crate::transport::Transport;
318
433
  "queued_messages": take_array(full.get("queued_messages"), 8),
319
434
  "results": full.get("results").cloned().unwrap_or_else(|| json!({})),
320
435
  "latest_results": take_array(full.get("latest_results"), 5),
436
+ "readiness": full.get("readiness").cloned().unwrap_or_else(|| json!({})),
321
437
  "coordinator": compact_object(full.get("coordinator"), &["status", "pid", "metadata_ok", "schema_ok"]),
322
438
  "last_events": take_array_tail(full.get("last_events"), 10),
323
439
  })
@@ -377,7 +493,7 @@ use crate::transport::Transport;
377
493
  };
378
494
  Value::Array(
379
495
  tasks.iter()
380
- .map(|task| compact_object(Some(task), &["id", "title", "status", "assignee", "type"]))
496
+ .map(|task| compact_object(Some(task), &["id", "title", "status", "assignee", "type", "accepted_result_id"]))
381
497
  .collect(),
382
498
  )
383
499
  }
@@ -410,30 +526,63 @@ use crate::transport::Transport;
410
526
  Value::Array(items.iter().skip(start).cloned().collect())
411
527
  }
412
528
 
413
- fn count_rows(conn: &rusqlite::Connection, table: &str) -> Result<i64, CliError> {
414
- let sql = format!("select count(*) from {table}");
415
- conn.query_row(&sql, [], |row| row.get::<_, i64>(0))
416
- .map_err(|e| CliError::Runtime(e.to_string()))
529
+ fn count_rows(
530
+ conn: &rusqlite::Connection,
531
+ table: &str,
532
+ owner_team_id: Option<&str>,
533
+ ) -> Result<i64, CliError> {
534
+ match owner_team_id {
535
+ Some(team) => {
536
+ let sql = format!("select count(*) from {table} where owner_team_id = ?1");
537
+ conn.query_row(&sql, [team], |row| row.get::<_, i64>(0))
538
+ .map_err(|e| CliError::Runtime(e.to_string()))
539
+ }
540
+ None => {
541
+ let sql = format!("select count(*) from {table}");
542
+ conn.query_row(&sql, [], |row| row.get::<_, i64>(0))
543
+ .map_err(|e| CliError::Runtime(e.to_string()))
544
+ }
545
+ }
417
546
  }
418
547
 
419
548
  fn count_where_status(
420
549
  conn: &rusqlite::Connection,
421
550
  table: &str,
551
+ owner_team_id: Option<&str>,
422
552
  status: &str,
423
553
  ) -> Result<i64, CliError> {
424
- let sql = format!("select count(*) from {table} where status = ?1");
425
- conn.query_row(&sql, [status], |row| row.get::<_, i64>(0))
426
- .map_err(|e| CliError::Runtime(e.to_string()))
554
+ match owner_team_id {
555
+ Some(team) => {
556
+ let sql = format!("select count(*) from {table} where status = ?1 and owner_team_id = ?2");
557
+ conn.query_row(&sql, params![status, team], |row| row.get::<_, i64>(0))
558
+ .map_err(|e| CliError::Runtime(e.to_string()))
559
+ }
560
+ None => {
561
+ let sql = format!("select count(*) from {table} where status = ?1");
562
+ conn.query_row(&sql, [status], |row| row.get::<_, i64>(0))
563
+ .map_err(|e| CliError::Runtime(e.to_string()))
564
+ }
565
+ }
427
566
  }
428
567
 
429
- fn agent_health(conn: &rusqlite::Connection) -> Result<Value, CliError> {
430
- let mut stmt = conn
431
- .prepare(
568
+ fn agent_health(conn: &rusqlite::Connection, owner_team_id: Option<&str>) -> Result<Value, CliError> {
569
+ let sql = match owner_team_id {
570
+ Some(_) => {
571
+ "select agent_id, status, last_output_at, context_usage_pct, current_task_id, updated_at, owner_team_id
572
+ from agent_health where owner_team_id = ?1 order by agent_id"
573
+ }
574
+ None => {
432
575
  "select agent_id, status, last_output_at, context_usage_pct, current_task_id, updated_at, owner_team_id
433
- from agent_health order by agent_id",
434
- )
576
+ from agent_health order by agent_id"
577
+ }
578
+ };
579
+ let mut stmt = conn
580
+ .prepare(sql)
435
581
  .map_err(|e| CliError::Runtime(e.to_string()))?;
436
- let mut rows = stmt.query([]).map_err(|e| CliError::Runtime(e.to_string()))?;
582
+ let mut rows = match owner_team_id {
583
+ Some(team) => stmt.query(params![team]).map_err(|e| CliError::Runtime(e.to_string()))?,
584
+ None => stmt.query([]).map_err(|e| CliError::Runtime(e.to_string()))?,
585
+ };
437
586
  let mut out = Map::new();
438
587
  while let Some(row) = rows.next().map_err(|e| CliError::Runtime(e.to_string()))? {
439
588
  let agent_id: String = row.get(0).map_err(|e| CliError::Runtime(e.to_string()))?;
@@ -456,7 +456,7 @@ Hint: team-agent inbox leader";
456
456
  // Rust always does CmdResult::from_json -> CmdOutput::Json (wrong shape).
457
457
  #[test]
458
458
  fn red_cmd_doctor_comms_human_is_boundary_text_plus_sorted_json() {
459
- const COMMS_BOUNDARY_TEXT: &str = "validates live pane binding consistency. Does NOT perform live runtime message round-trip. comms contract suite deferred to 0.2.9 (test files not shipped). (zero token, zero pollution)";
459
+ const COMMS_BOUNDARY_TEXT: &str = "validates live pane binding consistency and zero-token comms contracts. Does NOT perform live runtime message round-trip. (zero token, zero pollution)";
460
460
  let args = DoctorArgs {
461
461
  spec: None,
462
462
  workspace: PathBuf::from("."),
@@ -506,4 +506,3 @@ Hint: team-agent inbox leader";
506
506
  assert_eq!(run(&["codex".to_string(), "-h".to_string()], Path::new(".")), ExitCode::Ok);
507
507
  assert_eq!(run(&["claude".to_string(), "-h".to_string()], Path::new(".")), ExitCode::Ok);
508
508
  }
509
-
@@ -216,27 +216,35 @@ use super::*;
216
216
  // the deterministic check sub-shapes (run_id is a random uuid; not value-locked). ────────────────
217
217
  #[test]
218
218
  fn comms_selftest_golden_boundary_scope_and_check_shapes() {
219
- let boundary = "validates live pane binding consistency. Does NOT perform live runtime message \
220
- round-trip. comms contract suite deferred to 0.2.9 (test files not shipped). \
221
- (zero token, zero pollution)";
219
+ let boundary = "validates live pane binding consistency and zero-token comms contracts. \
220
+ Does NOT perform live runtime message round-trip. (zero token, zero pollution)";
222
221
  let ws = tmp_workspace();
223
222
  let v = diagnose_port::comms_selftest(&ws, None, None).expect("comms_selftest");
224
223
  let obj = v.as_object().expect("comms dict");
225
224
  assert_eq!(v["boundary"], json!(boundary), "golden COMMS_BOUNDARY_TEXT prefix (comms.py:11-14)");
226
225
  assert_eq!(v["scope"], json!("binding_consistency"), "golden scope");
227
- assert_eq!(v["status"], json!("pass"), "empty-state selftest passes (all checks pass/deferred)");
226
+ assert_eq!(
227
+ v["status"],
228
+ json!("fail"),
229
+ "empty workspace has no runtime receiver binding, so comms gate must not pass"
230
+ );
228
231
  assert!(obj.contains_key("run_id"), "golden carries a run_id (uuid hex[:12])");
229
232
  assert!(!obj.contains_key("team"), "golden has NO `team` key");
230
233
  assert!(!obj.contains_key("gate"), "golden has NO `gate` key");
231
- assert_eq!(
232
- v["checks"]["contract_suite"],
233
- json!({
234
- "status": "deferred",
235
- "deferred_to": "0.2.9",
236
- "reason": "contract test files not shipped with package",
237
- "message": "comms contract verification deferred to 0.2.9; contract test files not shipped with package",
238
- }),
239
- "golden contract_suite check (comms.py:132-139)"
234
+ assert_eq!(v["checks"]["contract_suite"]["status"], json!("pass"));
235
+ assert_eq!(v["checks"]["contract_suite"]["failed"], json!([]));
236
+ let suite_names: Vec<&str> = v["checks"]["contract_suite"]["checks"]
237
+ .as_array()
238
+ .expect("contract suite checks")
239
+ .iter()
240
+ .filter_map(|check| check.get("name").and_then(Value::as_str))
241
+ .collect();
242
+ assert!(
243
+ suite_names.contains(&"message_store_schema")
244
+ && suite_names.contains(&"message_token_shape")
245
+ && suite_names.contains(&"result_notification_render")
246
+ && suite_names.contains(&"leader_projection_owner_team"),
247
+ "contract suite must be executable, not deferred: {suite_names:?}"
240
248
  );
241
249
  assert_eq!(
242
250
  v["checks"]["provider_sdk_calls"]["calls"],
@@ -271,6 +279,8 @@ use super::*;
271
279
  // {ok,scanned,orphans,dry_run:true,scanned_at,action_required}. RUST mod.rs:534-535 stub
272
280
  // {ok,confirm,cleaned}. scanned/scanned_at are machine/clock-derived; lock the deterministic ones. RED.
273
281
  #[test]
282
+ #[ignore = "real-machine: cleanup_orphans scans machine-wide ta-* tmux/process residue"]
283
+ #[serial_test::file_serial(tmux)]
274
284
  fn cleanup_orphans_dryrun_golden_envelope() {
275
285
  let v = diagnose_port::cleanup_orphans(/*confirm=*/ false).expect("cleanup_orphans");
276
286
  let obj = v.as_object().expect("cleanup dict");
@@ -263,6 +263,8 @@ fn seed_team_spec(ws: &std::path::Path) {
263
263
  agent: "w1".to_string(),
264
264
  workspace: ws.clone(),
265
265
  tail: 20,
266
+ head: None,
267
+ search: None,
266
268
  allow_raw_screen: true,
267
269
  json: true,
268
270
  };
@@ -4,6 +4,39 @@ struct TeamSocketGuard {
4
4
  ws: std::path::PathBuf,
5
5
  }
6
6
 
7
+ struct EnvGuard {
8
+ previous: Vec<(&'static str, Option<String>)>,
9
+ }
10
+
11
+ impl EnvGuard {
12
+ fn unset(keys: &[&'static str]) -> Self {
13
+ let previous = keys
14
+ .iter()
15
+ .map(|key| (*key, std::env::var(key).ok()))
16
+ .collect::<Vec<_>>();
17
+ for key in keys {
18
+ unsafe {
19
+ std::env::remove_var(key);
20
+ }
21
+ }
22
+ Self { previous }
23
+ }
24
+ }
25
+
26
+ impl Drop for EnvGuard {
27
+ fn drop(&mut self) {
28
+ for (key, value) in self.previous.drain(..).rev() {
29
+ unsafe {
30
+ if let Some(value) = value {
31
+ std::env::set_var(key, value);
32
+ } else {
33
+ std::env::remove_var(key);
34
+ }
35
+ }
36
+ }
37
+ }
38
+ }
39
+
7
40
  impl Drop for TeamSocketGuard {
8
41
  fn drop(&mut self) {
9
42
  crate::tmux_backend::TmuxBackend::for_workspace(&self.ws).kill_server();
@@ -122,7 +155,18 @@ fn current_uid() -> Option<String> {
122
155
  #[ignore = "real-machine: quick-start --yes spawns a real team (tmux) + coordinator daemon"]
123
156
  fn run_dispatches_quick_start_compiles_spec() {
124
157
  // The full quick-start path spawns workers + the coordinator; on a real machine we assert it
125
- // dispatched to cmd_quick_start (team.spec.yaml compiled under the team dir) + ExitCode::Ok.
158
+ // dispatched to cmd_quick_start (team.spec.yaml compiled under the team dir). With no positive
159
+ // caller pane, honest-readiness reports leader_receiver_unbound and exits nonzero.
160
+ let _env = EnvGuard::unset(&[
161
+ "TMUX",
162
+ "TMUX_PANE",
163
+ "TEAM_AGENT_ID",
164
+ "TEAM_AGENT_TEAM_ID",
165
+ "TEAM_AGENT_LEADER_PANE_ID",
166
+ "TEAM_AGENT_LEADER_SESSION_UUID",
167
+ "TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE",
168
+ "TEAM_AGENT_LEADER_PROVIDER",
169
+ ]);
126
170
  let dir = std::env::temp_dir().join(format!("ta-cli-qs-{}", std::process::id()));
127
171
  std::fs::create_dir_all(dir.join("agents")).unwrap();
128
172
  std::fs::write(dir.join("TEAM.md"), "---\nname: t\nobjective: o\nprovider: codex\n---\n\nteam.\n").unwrap();
@@ -134,7 +178,11 @@ fn current_uid() -> Option<String> {
134
178
  let argv = vec!["quick-start".to_string(), dir.to_string_lossy().to_string(), "--yes".to_string()];
135
179
  let exit = run(&argv, Path::new("."));
136
180
  assert!(dir.join("team.spec.yaml").exists(), "quick-start must compile the spec under the team dir");
137
- assert_eq!(exit, ExitCode::Ok);
181
+ assert_eq!(
182
+ exit,
183
+ ExitCode::Error,
184
+ "quick-start must still dispatch and compile, but an unbound leader receiver is an honest-readiness failure, not ExitCode::Ok"
185
+ );
138
186
  }
139
187
 
140
188
  // =========================================================================
@@ -152,6 +200,7 @@ fn current_uid() -> Option<String> {
152
200
  let ws = crate::model::paths::team_workspace(&team).unwrap();
153
201
  let _guard = TeamSocketGuard { ws };
154
202
  let args = QuickStartArgs {
203
+ workspace: crate::model::paths::team_workspace(&team).unwrap(),
155
204
  agents_dir: team.clone(),
156
205
  name: None,
157
206
  team_id: None,
@@ -177,6 +226,7 @@ fn current_uid() -> Option<String> {
177
226
  std::fs::write(team.join("TEAM.md"), DELEG_TEAM_MD).unwrap();
178
227
  std::fs::write(team.join("agents").join("broken.md"), DELEG_INVALID_ROLE).unwrap();
179
228
  let args = QuickStartArgs {
229
+ workspace: crate::model::paths::team_workspace(&team).unwrap(),
180
230
  agents_dir: team,
181
231
  name: None,
182
232
  team_id: None,
@@ -198,7 +248,13 @@ fn current_uid() -> Option<String> {
198
248
  #[test]
199
249
  fn cli_restart_missing_spec_surfaces_real_teamselect() {
200
250
  let ws = deleg_uniq_dir("restart"); // no team.spec.yaml
201
- let args = RestartArgs { workspace: ws, team: None, allow_fresh: false, json: true };
251
+ let args = RestartArgs {
252
+ workspace: ws,
253
+ team: None,
254
+ allow_fresh: false,
255
+ session_converge_deadline_ms: None,
256
+ json: true,
257
+ };
202
258
  let text = outcome_text(cmd_restart(&args));
203
259
  assert!(
204
260
  text.contains("missing spec for restart"),
@@ -2,6 +2,8 @@
2
2
  //! 每子命令的 clap-style arg 结构 / 五行 summary 计数桶。
3
3
 
4
4
  use super::*;
5
+ use crate::provider::Provider;
6
+ use crate::transport::PaneId;
5
7
 
6
8
  // =============================================================================
7
9
  // ERRORS / EXIT(helpers.py `_emit_cli_error` / `_cli_error_payload`)
@@ -253,6 +255,7 @@ pub struct InteractionCounts {
253
255
  /// `quick-start`(`parser.py:105`)。
254
256
  #[derive(Debug, Clone, PartialEq, Eq)]
255
257
  pub struct QuickStartArgs {
258
+ pub workspace: PathBuf,
256
259
  pub agents_dir: PathBuf,
257
260
  pub name: Option<String>,
258
261
  pub team_id: Option<String>,
@@ -362,6 +365,18 @@ pub struct ClaimLeaderArgs {
362
365
  pub json: bool,
363
366
  }
364
367
 
368
+ /// `attach-leader` public CLI args. `cmd_attach_leader` consumes the typed pane/provider
369
+ /// fields and returns/writes a `leader_receiver` binding through the leader lease port.
370
+ #[derive(Debug, Clone, PartialEq, Eq)]
371
+ pub struct AttachLeaderArgs {
372
+ pub workspace: PathBuf,
373
+ pub team: Option<String>,
374
+ pub pane: Option<PaneId>,
375
+ pub provider: Provider,
376
+ pub confirm: bool,
377
+ pub json: bool,
378
+ }
379
+
365
380
  /// `identity`(`parser.py:256`)。
366
381
  #[derive(Debug, Clone, PartialEq, Eq)]
367
382
  pub struct IdentityArgs {
@@ -385,6 +400,7 @@ pub struct RestartArgs {
385
400
  pub workspace: PathBuf,
386
401
  pub team: Option<String>,
387
402
  pub allow_fresh: bool,
403
+ pub session_converge_deadline_ms: Option<u64>,
388
404
  pub json: bool,
389
405
  }
390
406
 
@@ -600,6 +616,8 @@ pub struct PeekArgs {
600
616
  pub agent: String,
601
617
  pub workspace: PathBuf,
602
618
  pub tail: usize,
619
+ pub head: Option<usize>,
620
+ pub search: Option<String>,
603
621
  pub allow_raw_screen: bool,
604
622
  pub json: bool,
605
623
  }
@@ -158,7 +158,7 @@ pub fn compile_team(team_dir: &Path) -> Result<Value, ModelError> {
158
158
  let tools = required_tools(&meta, &path)?;
159
159
  let prompt_inline = non_empty_trimmed(&body).unwrap_or_else(|| role.clone());
160
160
  agent_ids.push(id.clone());
161
- agents.push(map(vec![
161
+ let mut agent_items = vec![
162
162
  ("id", Value::Str(id.clone())),
163
163
  ("role", Value::Str(role.clone())),
164
164
  ("provider", Value::Str(provider)),
@@ -186,7 +186,11 @@ pub fn compile_team(team_dir: &Path) -> Result<Value, ModelError> {
186
186
  ),
187
187
  ]),
188
188
  ),
189
- ]));
189
+ ];
190
+ if let Some(profile) = string_field(&meta, "profile") {
191
+ agent_items.push(("profile", Value::Str(profile)));
192
+ }
193
+ agents.push(map(agent_items));
190
194
  }
191
195
 
192
196
  let default_assignee = agent_ids.first().cloned().unwrap_or_default();
@@ -409,10 +413,16 @@ fn resolve_model(role_meta: &Value, team_meta: &Value, provider: &str) -> Value
409
413
  if let Some(model) = string_field(role_meta, "model") {
410
414
  return Value::Str(model);
411
415
  }
412
- provider_model(team_meta, provider)
416
+ if let Some(model) = provider_model(team_meta, provider)
413
417
  .or_else(|| string_field(team_meta, "default_model"))
414
- .map(Value::Str)
415
- .or_else(|| builtin_provider_model(provider).map(|m| Value::Str(m.to_string())))
418
+ {
419
+ return Value::Str(model);
420
+ }
421
+ if role_meta.get("profile").is_some() {
422
+ return Value::Null;
423
+ }
424
+ builtin_provider_model(provider)
425
+ .map(|m| Value::Str(m.to_string()))
416
426
  .unwrap_or(Value::Null)
417
427
  }
418
428