@team-agent/installer 0.5.111 → 0.5.112

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 (38) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +1 -743
  4. package/crates/team-agent/src/cli/diagnose.rs +2 -424
  5. package/crates/team-agent/src/cli/emit.rs +31 -275
  6. package/crates/team-agent/src/cli/mod.rs +2 -9
  7. package/crates/team-agent/src/cli/spec.rs +0 -3
  8. package/crates/team-agent/src/cli/tests/leader_watch.rs +2 -86
  9. package/crates/team-agent/src/cli/tests/main_preserved.rs +15 -140
  10. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +1 -258
  11. package/crates/team-agent/src/cli/tests/mod.rs +0 -2
  12. package/crates/team-agent/src/cli/types.rs +4 -108
  13. package/crates/team-agent/src/coordinator/health.rs +3 -283
  14. package/crates/team-agent/src/coordinator/mod.rs +0 -5
  15. package/crates/team-agent/src/coordinator/tests/basics.rs +0 -9
  16. package/crates/team-agent/src/coordinator/tests/mod.rs +0 -1
  17. package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -17
  18. package/crates/team-agent/src/coordinator/types.rs +1 -21
  19. package/crates/team-agent/src/db/message_store.rs +0 -42
  20. package/crates/team-agent/src/db/migration.rs +49 -9
  21. package/crates/team-agent/src/db/schema.rs +27 -20
  22. package/crates/team-agent/src/leader/owner_bind.rs +0 -24
  23. package/crates/team-agent/src/leader/tests/byte_findings.rs +0 -20
  24. package/crates/team-agent/src/leader/tests/identity.rs +0 -32
  25. package/crates/team-agent/src/leader/tests/lease_claim.rs +0 -55
  26. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -51
  27. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +23 -23
  28. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +29 -55
  29. package/crates/team-agent/src/messaging/mod.rs +0 -2
  30. package/crates/team-agent/src/messaging/results.rs +0 -58
  31. package/crates/team-agent/src/messaging/tests/runtime.rs +0 -46
  32. package/crates/team-agent/src/state/repository.rs +1 -5
  33. package/package.json +4 -4
  34. package/templates/team_state.md +1 -1
  35. package/crates/team-agent/src/cli/tests/peer_allow.rs +0 -164
  36. package/crates/team-agent/src/cli/tests/verb_validate.rs +0 -195
  37. package/crates/team-agent/src/coordinator/tests/watch.rs +0 -223
  38. package/crates/team-agent/src/messaging/peers.rs +0 -34
@@ -1,7 +1,7 @@
1
1
  //!
2
2
  //! `team.db` schema 初始化(真相源 `message_store/schema.py` + `schema_migration.py`)。
3
3
  //!
4
- //! slice 1:fresh DB schema —— 8 表 DDL、6 索引、ALTER 式列迁移、agent_health 重建、
4
+ //! slice 1:fresh DB schema —— 7 表 DDL、6 索引、ALTER 式列迁移、agent_health 重建、
5
5
  //! WAL/busy_timeout pragmas、`user_version = SCHEMA_VERSION`。
6
6
  //! slice 2(待做):列**顺序**漂移的整表 rebuild(`ensure_table_layout`/`_rebuild_tables`)
7
7
  //! 与 `schema_diagnosis`,验 legacy_team_db_fixture + schema_migration 契约。
@@ -15,14 +15,13 @@ use crate::db::DbError;
15
15
  /// `schema.py:90`。
16
16
  pub const SCHEMA_VERSION: i64 = 5;
17
17
 
18
- /// 8 张表的 DDL(逐字照搬 `schema.py:initialize_schema` 的内联建表;含 `if not exists`)。
18
+ /// 7 张表的 DDL(逐字照搬 `schema.py:initialize_schema` 的内联建表;含 `if not exists`)。
19
19
  /// 顺序与 Python 一致(leader_notification_log 在 ensure 块后创建)。
20
20
  const CREATE_MESSAGES: &str = "create table if not exists messages (\n message_id text primary key,\n owner_team_id text,\n task_id text,\n sender text,\n recipient text,\n reply_to text,\n requires_ack integer,\n status text,\n content text,\n presentation text not null default '{\"sink\":\"leader\",\"class\":\"message\"}',\n artifact_refs text,\n created_at text,\n updated_at text,\n delivered_at text,\n acknowledged_at text,\n error text,\n delivery_attempts integer not null default 0\n )";
21
21
  const CREATE_RESULTS: &str = "create table if not exists results (\n result_id text primary key,\n owner_team_id text,\n task_id text not null,\n agent_id text not null,\n envelope text not null,\n status text not null,\n created_at text not null\n )";
22
22
  const CREATE_SCHEDULED_EVENTS: &str = "create table if not exists scheduled_events (\n id integer primary key,\n owner_team_id text,\n due_at text not null,\n target text not null,\n kind text not null,\n payload_json text not null,\n status text not null,\n created_at text not null,\n fired_at text,\n result_json text\n )";
23
23
  const CREATE_DELIVERY_TOKENS: &str = "create table if not exists delivery_tokens (\n message_id text primary key,\n unique_token text not null,\n injected_at text not null,\n visible_at text,\n consumed_at text,\n failed_at text,\n failure_reason text\n )";
24
24
  const CREATE_AGENT_HEALTH: &str = "create table if not exists agent_health (\n owner_team_id text,\n agent_id text not null,\n status text not null,\n last_output_at text,\n context_usage_pct integer,\n current_task_id text,\n updated_at text not null,\n unique(owner_team_id, agent_id)\n )";
25
- const CREATE_PEER_ALLOWLIST: &str = "create table if not exists peer_allowlist (\n a text not null,\n b text not null,\n created_at text not null,\n primary key (a, b)\n )";
26
25
  const CREATE_RESULT_WATCHERS: &str = "create table if not exists result_watchers (\n watcher_id text primary key,\n owner_team_id text,\n task_id text,\n agent_id text,\n message_id text,\n leader_id text not null,\n recipient text,\n status text not null,\n created_at text not null,\n completed_at text,\n result_id text,\n notified_message_id text,\n error text\n )";
27
26
  const CREATE_LEADER_NOTIFICATION_LOG: &str = "create table if not exists leader_notification_log (\n result_id text not null,\n owner_team_id text not null default '',\n owner_epoch integer not null default 0,\n leader_session_uuid text,\n notified_message_id text not null,\n notified_at text not null,\n leader_pane_id_at_notify text,\n envelope_content_hash text,\n primary key (result_id, owner_team_id, owner_epoch)\n )";
28
27
  const CREATE_AGENT_HEALTH_NEW: &str = "create table agent_health_new (\n owner_team_id text,\n agent_id text not null,\n status text not null,\n last_output_at text,\n context_usage_pct integer,\n current_task_id text,\n updated_at text not null,\n unique(owner_team_id, agent_id)\n )";
@@ -96,7 +95,6 @@ const AGENT_HEALTH_COLUMNS: &[&str] = &[
96
95
  "current_task_id",
97
96
  "updated_at",
98
97
  ];
99
- const PEER_ALLOWLIST_COLUMNS: &[&str] = &["a", "b", "created_at"];
100
98
  const RESULT_WATCHER_COLUMNS: &[&str] = &[
101
99
  "owner_team_id",
102
100
  "watcher_id",
@@ -249,7 +247,6 @@ pub fn initialize_schema(
249
247
  CREATE_SCHEDULED_EVENTS,
250
248
  CREATE_DELIVERY_TOKENS,
251
249
  CREATE_AGENT_HEALTH,
252
- CREATE_PEER_ALLOWLIST,
253
250
  CREATE_RESULT_WATCHERS,
254
251
  ] {
255
252
  tx.execute(ddl, [])?;
@@ -293,7 +290,6 @@ pub fn initialize_schema(
293
290
  )?;
294
291
  ensure_table_columns(&tx, "delivery_tokens", DELIVERY_TOKEN_COLUMNS, &[])?;
295
292
  migrate_agent_health_owner_team_id(&tx)?;
296
- ensure_table_columns(&tx, "peer_allowlist", PEER_ALLOWLIST_COLUMNS, &[])?;
297
293
  ensure_table_columns(
298
294
  &tx,
299
295
  "result_watchers",
@@ -333,7 +329,28 @@ mod tests {
333
329
  conn
334
330
  }
335
331
 
336
- // golden 由 Python initialize_schema 新建 DB 取(team-agent-public@439bef8)。
332
+ #[test]
333
+ fn retired_peer_allowlist_is_neither_created_nor_managed() {
334
+ let conn = fresh();
335
+ assert!(table_layout(&conn, "peer_allowlist").unwrap().is_empty());
336
+ // Legacy residue may have an obsolete layout. Initialization must leave
337
+ // its data alone, not rebuild/drop it or require it for schema health.
338
+ conn.execute_batch(
339
+ "create table peer_allowlist (legacy_payload text);\n insert into peer_allowlist values ('preserve-this-row');",
340
+ )
341
+ .unwrap();
342
+ initialize_schema(&conn, None).unwrap();
343
+ assert!(crate::db::migration::ensure_table_layout(&conn, SCHEMA_VERSION, None)
344
+ .unwrap()
345
+ .is_empty());
346
+ assert_eq!(table_layout(&conn, "peer_allowlist").unwrap(), ["legacy_payload"]);
347
+ let value: String = conn.query_row("select legacy_payload from peer_allowlist", [], |row| row.get(0)).unwrap();
348
+ assert_eq!(value, "preserve-this-row");
349
+ let version: i64 = conn.query_row("pragma user_version", [], |row| row.get(0)).unwrap();
350
+ assert_eq!(version, 5);
351
+ }
352
+
353
+ // Fresh managed schema, excluding retired legacy tables.
337
354
  #[test]
338
355
  fn fresh_schema_matches_python_sqlite_master() {
339
356
  let conn = fresh();
@@ -356,7 +373,6 @@ mod tests {
356
373
  ("idx_scheduled_events_owner_team_id", "index"),
357
374
  ("leader_notification_log", "table"),
358
375
  ("messages", "table"),
359
- ("peer_allowlist", "table"),
360
376
  ("result_watchers", "table"),
361
377
  ("results", "table"),
362
378
  ("scheduled_events", "table"),
@@ -419,9 +435,9 @@ mod tests {
419
435
  assert_eq!(idx, 6, "二次 init 后 6 个 named index 仍在");
420
436
  }
421
437
 
422
- // 全 8 表逐列 (name, type, notnull, dflt, pk) golden(Python initialize_schema table_info)。
438
+ // Managed tables: (name, type, notnull, dflt, pk).
423
439
  #[test]
424
- fn per_column_table_info_all_eight_tables() {
440
+ fn per_column_table_info_managed_tables() {
425
441
  type Col = (&'static str, &'static str, i64, Option<&'static str>, i64);
426
442
  let golden: &[(&str, &[Col])] = &[
427
443
  (
@@ -503,14 +519,6 @@ mod tests {
503
519
  ("updated_at", "TEXT", 1, None, 0),
504
520
  ],
505
521
  ),
506
- (
507
- "peer_allowlist",
508
- &[
509
- ("a", "TEXT", 1, None, 1),
510
- ("b", "TEXT", 1, None, 2),
511
- ("created_at", "TEXT", 1, None, 0),
512
- ],
513
- ),
514
522
  (
515
523
  "result_watchers",
516
524
  &[
@@ -582,7 +590,7 @@ mod tests {
582
590
  ];
583
591
  let got: Vec<(&str, &str)> = idx.iter().map(|(n, s)| (n.as_str(), s.as_str())).collect();
584
592
  assert_eq!(got, want);
585
- // 7 个 sqlite_autoindex(每个有 pk/unique 约束的表各一)。
593
+ // 6 个 sqlite_autoindex(每个有 pk/unique 约束的表各一)。
586
594
  let auto: Vec<String> = conn
587
595
  .prepare(
588
596
  "select name from sqlite_master where type='index' and sql is null order by name",
@@ -599,7 +607,6 @@ mod tests {
599
607
  "sqlite_autoindex_delivery_tokens_1",
600
608
  "sqlite_autoindex_leader_notification_log_1",
601
609
  "sqlite_autoindex_messages_1",
602
- "sqlite_autoindex_peer_allowlist_1",
603
610
  "sqlite_autoindex_result_watchers_1",
604
611
  "sqlite_autoindex_results_1",
605
612
  ]
@@ -17,30 +17,6 @@ use super::{
17
17
  OwnerBindResult, TeamOwner,
18
18
  };
19
19
 
20
- // ── leader::identity — leader_identity / 身份上下文 ──
21
-
22
- /// `leader_identity`(card §47;`__init__.py:355`)。`team-agent identity` 入口。
23
- /// 返回 uuid_prefix + 身份字段(JSON dict,CLI 直出)。
24
- pub fn leader_identity(workspace: &Path, team: Option<&str>) -> Result<Value, LeaderError> {
25
- let state = crate::state::persist::load_runtime_state(workspace)?;
26
- let identity = leader_identity_context(workspace, team, Some(&state))?;
27
- Ok(json!({
28
- "ok": true,
29
- "uuid_prefix": prefix(identity.leader_session_uuid.as_str(), 12),
30
- "machine_fingerprint": identity.machine_fingerprint,
31
- "workspace_abspath": identity.workspace_abspath.to_string_lossy(),
32
- "os_user": identity.os_user,
33
- "team_id": identity.team_id.as_str(),
34
- "current_pane_id": std::env::var("TEAM_AGENT_LEADER_PANE_ID")
35
- .ok()
36
- .filter(|v| !v.is_empty())
37
- .or_else(|| std::env::var("TMUX_PANE").ok().filter(|v| !v.is_empty())),
38
- "last_seen_at": get_path_str(&state, &["leader_receiver", "attached_at"])
39
- .or_else(|| get_path_str(&state, &["leader_receiver", "last_seen_at"])),
40
- "source": serde_json::to_value(identity.leader_session_uuid_source)?,
41
- }))
42
- }
43
-
44
20
  /// `_leader_identity_context`(`__init__.py:192`)。派生 leader 身份上下文(override / state / derive)。
45
21
  pub fn leader_identity_context(
46
22
  workspace: &Path,
@@ -353,23 +353,3 @@ fn attach_leader_to_state_nonfirst_does_not_overwrite_owner_or_advance_epoch() {
353
353
  "non-first-time attach must NOT advance owner_epoch (golden keeps 3); Rust bumps to 4"
354
354
  );
355
355
  }
356
-
357
- // C3 [WARN] — leader_identity.current_pane_id honors TEAM_AGENT_LEADER_PANE_ID priority (golden
358
- // :366 = env(TEAM_AGENT_LEADER_PANE_ID) or env(TMUX_PANE) or None). Rust owner_bind.rs:32 reads
359
- // TMUX_PANE only. RED: with TEAM_AGENT_LEADER_PANE_ID set + TMUX_PANE unset, current_pane_id must be
360
- // the LEADER_PANE_ID. (Sibling gap, noted: last_seen_at = receiver.attached_at OR receiver.last_seen_at;
361
- // Rust reads attached_at only.)
362
- #[test]
363
- #[serial_test::serial(env)]
364
- fn c3_leader_identity_current_pane_honors_leader_pane_id_env() {
365
- let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
366
- let _e = EnvGuard::apply(&[
367
- ("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None),
368
- ("TEAM_AGENT_LEADER_PANE_ID", Some("%foo")),
369
- ("TMUX_PANE", None),
370
- ]);
371
- let ws = p2_temp_ws("c3_pane_env");
372
- let v = leader_identity(&ws, None).unwrap();
373
- assert_eq!(v["current_pane_id"], serde_json::json!("%foo"),
374
- "golden :366 current_pane_id = TEAM_AGENT_LEADER_PANE_ID or TMUX_PANE or None; Rust reads TMUX_PANE only");
375
- }
@@ -23,38 +23,6 @@ fn leader_identity_context_derives_when_no_override_no_state() {
23
23
  assert_eq!(id.leader_session_uuid.as_str().len(), 32);
24
24
  }
25
25
 
26
- // leader_identity:CLI 直出 dict(__init__.py:355-369)。unimplemented → RED。
27
- // 强化:uuid_prefix 必须 == leader_identity_context 派生 uuid 的前 12 hex(绑到真值,
28
- // 而非任意 12 字符);整个 dict 的 machine_fingerprint/os_user/team_id/source 必须与 context 一致。
29
- #[test]
30
- #[serial_test::serial(env)]
31
- fn leader_identity_dict_ties_prefix_and_fields_to_derived_context() {
32
- let ws = std::env::temp_dir().join(format!("ta_rs_lid_{}", std::process::id()));
33
- std::fs::create_dir_all(&ws).unwrap();
34
- // 空 state → context 走 derive(无 override/无 state uuid)。
35
- let ctx = leader_identity_context(&ws, None, Some(&serde_json::json!({}))).unwrap();
36
- let expected_prefix = &ctx.leader_session_uuid.as_str()[..12];
37
- let v = leader_identity(&ws, None).unwrap();
38
- assert_eq!(v["ok"], serde_json::json!(true));
39
- // uuid_prefix 绑到派生真值的前 12 hex(错的 12 字符串会被抓)。
40
- assert_eq!(v["uuid_prefix"].as_str().unwrap(), expected_prefix);
41
- // 其余身份字段与 context 字节一致。
42
- assert_eq!(
43
- v["machine_fingerprint"].as_str().unwrap(),
44
- ctx.machine_fingerprint
45
- );
46
- assert_eq!(v["os_user"].as_str().unwrap(), ctx.os_user);
47
- assert_eq!(v["team_id"].as_str().unwrap(), ctx.team_id.as_str());
48
- // source == 派生侧 "derived"(无 override → 不是 "override"/"env")。
49
- assert_eq!(v["source"], serde_json::json!("derived"));
50
- assert_eq!(
51
- ctx.leader_session_uuid_source,
52
- LeaderSessionUuidSource::Derived
53
- );
54
- // CLI 直出形态:current_pane_id / last_seen_at 在无 env/无 receiver 时为 null。
55
- assert_eq!(v["last_seen_at"], serde_json::Value::Null);
56
- }
57
-
58
26
  // =====================================================================
59
27
  // 13. leader_start_plan(unimplemented → RED):钉 mode 选择 + leader_env 导出键。
60
28
  // =====================================================================
@@ -398,61 +398,6 @@ fn claim_lease_no_incident_caller_not_live_refused_under_n_rn3_any_live_pane() {
398
398
  assert_eq!(r.reason, Some(LeaseReason::CallerPaneNotLive));
399
399
  }
400
400
 
401
- // ── 14b. leader_identity dict — workspace_abspath key (golden __init__.py:363) ──
402
- //
403
- // LOCK→RED: leader_identity IS implemented (owner_bind.rs:22) but its dict
404
- // OMITS the golden `workspace_abspath` key (probe_lid.py shows it sits between
405
- // machine_fingerprint and os_user). This pins the missing key so the porter
406
- // must add it. Fails today (key absent → null) = RED against the omission.
407
- #[test]
408
- #[serial_test::serial(env)]
409
- fn leader_identity_dict_includes_workspace_abspath_key() {
410
- let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
411
- let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
412
- let ws = p2_temp_ws("lid_wsabs");
413
- let v = leader_identity(&ws, None).unwrap();
414
- let ctx = leader_identity_context(&ws, None, Some(&serde_json::json!({}))).unwrap();
415
- // golden key present + equals the resolved workspace abspath from context.
416
- assert!(
417
- v.get("workspace_abspath").is_some(),
418
- "golden leader_identity dict carries 'workspace_abspath' (probe_lid.py)"
419
- );
420
- assert_eq!(
421
- v["workspace_abspath"].as_str().unwrap(),
422
- ctx.workspace_abspath.to_string_lossy(),
423
- "workspace_abspath must equal the context's resolved abspath"
424
- );
425
- }
426
-
427
- // LOCK — leader_identity key ORDER & full key set (golden probe_lid.py):
428
- // [ok, uuid_prefix, machine_fingerprint, workspace_abspath, os_user, team_id,
429
- // current_pane_id, last_seen_at, source]. Pins the 9-key surface CLI emits.
430
- #[test]
431
- #[serial_test::serial(env)]
432
- fn leader_identity_dict_has_exact_nine_golden_keys() {
433
- let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
434
- let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
435
- let ws = p2_temp_ws("lid_keys");
436
- let v = leader_identity(&ws, None).unwrap();
437
- let obj = v.as_object().expect("leader_identity → JSON object");
438
- for key in [
439
- "ok",
440
- "uuid_prefix",
441
- "machine_fingerprint",
442
- "workspace_abspath",
443
- "os_user",
444
- "team_id",
445
- "current_pane_id",
446
- "last_seen_at",
447
- "source",
448
- ] {
449
- assert!(
450
- obj.contains_key(key),
451
- "golden leader_identity dict must carry '{key}'"
452
- );
453
- }
454
- }
455
-
456
401
  // ═════════════════════════════════════════════════════════════════════════
457
402
  // WAVE-2 Lane B — leader-lease DIVERGENCE round (adversarial review @ 2cd71ce).
458
403
  // The lease gate is green but NOT byte-parity. Golden: leader/__init__.py
@@ -21,7 +21,6 @@ use std::sync::Mutex;
21
21
 
22
22
  use serde_json::{json, Value};
23
23
  use serial_test::serial;
24
- use team_agent::cli::{cmd_wait_ready, CmdOutput, WaitReadyArgs};
25
24
  use team_agent::lifecycle::{
26
25
  classify_restart_plan, restart_with_transport, RestartReport, ResumeDecision, StartMode,
27
26
  };
@@ -448,48 +447,7 @@ fn restart_refuses_first_send_at_null_running_resumable_session_incomplete_when_
448
447
  }
449
448
 
450
449
  #[test]
451
- #[ignore = "real-machine: provider session filesystem + restart/shutdown lifecycle convergence"]
452
- #[serial(env)]
453
- fn wait_ready_reports_session_capture_incomplete_for_running_resumable_worker_without_session() {
454
- let fixture = RestartFixture::new("wait-ready-session-incomplete", "codex");
455
- seed_running_state_without_session(&fixture.team, "codex", &fixture.spawn_cwd);
456
- let mut state = load_runtime_state(&fixture.team).unwrap();
457
- let mcp_config = fixture.team.join(".team/runtime/worker_a.mcp.json");
458
- std::fs::create_dir_all(mcp_config.parent().unwrap()).unwrap();
459
- std::fs::write(&mcp_config, "{}").unwrap();
460
- state["agents"]["worker_a"]["pane_id"] = json!("%1");
461
- state["agents"]["worker_a"]["mcp_config"] = json!(mcp_config.to_string_lossy().to_string());
462
- save_runtime_state(&fixture.team, &state).unwrap();
463
-
464
- let out = json_result(
465
- cmd_wait_ready(&WaitReadyArgs {
466
- workspace: fixture.team.clone(),
467
- timeout: 0.0,
468
- json: true,
469
- team: None,
470
- })
471
- .expect("wait-ready should return structured JSON"),
472
- );
473
-
474
- assert_eq!(
475
- out["ok"],
476
- json!(false),
477
- "wait-ready cannot report ready while a running Codex/Claude worker is missing captured session context; out={out}"
478
- );
479
- assert_eq!(
480
- out["reason"],
481
- json!("session_capture_incomplete"),
482
- "readiness reason must distinguish missing resumable session from generic workers_not_ready; out={out}"
483
- );
484
- assert!(
485
- out["readiness"]["session_capture_incomplete"].as_bool() == Some(true)
486
- && out["readiness"]["all_resumable_have_session"].as_bool() == Some(false),
487
- "readiness truth table must include all_resumable_have_session and session_capture_incomplete; out={out}"
488
- );
489
- }
490
-
491
- #[test]
492
- fn status_quick_start_and_wait_ready_surfaces_include_session_capture_completeness() {
450
+ fn status_and_quick_start_surfaces_include_session_capture_completeness() {
493
451
  let diagnose =
494
452
  std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/src/cli/diagnose.rs"))
495
453
  .unwrap();
@@ -500,7 +458,7 @@ fn status_quick_start_and_wait_ready_surfaces_include_session_capture_completene
500
458
  .unwrap();
501
459
 
502
460
  for (name, source) in [
503
- ("wait-ready", diagnose.as_str()),
461
+ ("status readiness formula", diagnose.as_str()),
504
462
  ("status --json", status.as_str()),
505
463
  ("quick-start readiness", launch.as_str()),
506
464
  ("quick-start --json", adapters.as_str()),
@@ -945,13 +903,6 @@ fn read_events(team: &Path) -> String {
945
903
  std::fs::read_to_string(team.join(".team/logs/events.jsonl")).unwrap_or_default()
946
904
  }
947
905
 
948
- fn json_result(result: team_agent::cli::CmdResult) -> Value {
949
- match result.output {
950
- CmdOutput::Json(value) => value,
951
- other => panic!("expected JSON command output, got {other:?}"),
952
- }
953
- }
954
-
955
906
  fn assert_no_agent_attribution_ambiguous<const N: usize>(
956
907
  state: &Value,
957
908
  expected_agents: [&str; N],
@@ -23,7 +23,7 @@ use std::time::{Duration, Instant};
23
23
 
24
24
  use serde_json::{json, Value};
25
25
  use serial_test::serial;
26
- use team_agent::cli::{cmd_preflight, emit, CmdOutput, PreflightArgs};
26
+ use team_agent::cli::{cmd_doctor, emit, CmdOutput, DoctorArgs};
27
27
  use team_agent::event_log::EventLog;
28
28
  use team_agent::message_store::MessageStore;
29
29
  use team_agent::state::persist::save_runtime_state;
@@ -334,11 +334,10 @@ fn red6_cli_restart_reports_and_top_level_error_logs_are_redacted() {
334
334
  env.scrub_tmux();
335
335
  env.assert_no_real_tmux();
336
336
  let workspace = env.workspace("red6");
337
- let spec = write_invalid_validate_spec(&workspace, &marker);
338
337
  let mut combined = String::new();
339
338
  for json_mode in [true, false] {
340
339
  let mut command = Command::new(crate::lifecycle::tests::test_binary_path());
341
- command.arg("validate").arg(&spec);
340
+ command.arg("profile").arg(credential_url(&marker)).arg("synthetic-profile");
342
341
  if json_mode {
343
342
  command.arg("--json");
344
343
  }
@@ -351,7 +350,7 @@ fn red6_cli_restart_reports_and_top_level_error_logs_are_redacted() {
351
350
  .expect("run top-level CLI error boundary");
352
351
  assert!(
353
352
  !output.status.success(),
354
- "invalid spec must reach CLI error"
353
+ "invalid profile subcommand must reach CLI error"
355
354
  );
356
355
  combined.push_str(&String::from_utf8_lossy(&output.stdout));
357
356
  combined.push_str(&String::from_utf8_lossy(&output.stderr));
@@ -391,15 +390,30 @@ fn red7_pane_and_profile_scrubbers_converge_without_losing_existing_policy() {
391
390
  let env = hermetic_guard::HermeticTestEnv::enter("redact-red7");
392
391
  let pane_policy_preserved = pane_excerpt_uses_shared_redaction(&env, &marker);
393
392
  let team = write_profile_fixture(env.root(), server.base_url(), &profile_token);
394
- let result = cmd_preflight(&PreflightArgs { team, json: true })
393
+ let result = cmd_doctor(&DoctorArgs {
394
+ spec: Some(team.join("TEAM.md")),
395
+ workspace: team,
396
+ gate: None,
397
+ comms: false,
398
+ team: None,
399
+ fix: false,
400
+ fix_schema: false,
401
+ cleanup_orphans: false,
402
+ confirm: false,
403
+ json: true,
404
+ })
395
405
  .expect("profile smoke returns a diagnostic report");
396
406
  let report = match result.output {
397
407
  CmdOutput::Json(value) => value,
398
- other => panic!("expected JSON preflight output, got {other:?}"),
408
+ other => panic!("expected JSON doctor output, got {other:?}"),
399
409
  };
400
- let text = report.to_string();
401
- let profile_policy_preserved = !text.contains(&marker)
402
- && !text.contains(&profile_token)
410
+ let report_text = report.to_string();
411
+ let text = report
412
+ .get("profile_smoke")
413
+ .unwrap_or_else(|| panic!("doctor must expose profile_smoke; report={report}"))
414
+ .to_string();
415
+ let profile_policy_preserved = !report_text.contains(&marker)
416
+ && !report_text.contains(&profile_token)
403
417
  && !text.contains(tail_sentinel)
404
418
  && text.contains(REDACTED)
405
419
  && text.contains("proxy.invalid:8443/path")
@@ -555,20 +569,6 @@ fn run_mcp_status_frames(
555
569
  .collect()
556
570
  }
557
571
 
558
- fn write_invalid_validate_spec(workspace: &Path, marker: &str) -> PathBuf {
559
- let path = workspace.join("invalid-provider.spec.yaml");
560
- fs::write(
561
- &path,
562
- format!(
563
- "version: 1\nteam:\n id: current\n name: current\n session_name: team-redaction-contract\n workspace: \"{}\"\nleader:\n provider: \"{}\"\nagents:\n - id: worker\n provider: fake\n model: fake\n role: Worker\n window: worker\ntasks: []\n",
564
- workspace.display(),
565
- credential_url(marker)
566
- ),
567
- )
568
- .unwrap();
569
- path
570
- }
571
-
572
572
  fn read_cli_error_logs(workspace: &Path) -> String {
573
573
  let dir = workspace.join(".team/logs");
574
574
  fs::read_dir(dir)
@@ -92,15 +92,10 @@ const SUBROOT_FULL_TRUST_PANE_AFTER_QUICKSTART: &str = r#"> You are in /private/
92
92
  │ directory: /private/tmp/…/teamdir │
93
93
  ╰───────────────────────────────────────╯
94
94
 
95
-
96
95
  › Find and fix a bug in @filename
97
96
 
98
97
  gpt-5.5 default · /private/tmp/ta-subroot-SUB-PROBE-1-trust-startup.uL9Boc/te…
99
98
 
100
-
101
-
102
-
103
-
104
99
  "#;
105
100
 
106
101
  fn tmp_dir(tag: &str) -> TempWorkspace {
@@ -419,8 +414,7 @@ fn contract_a_own_workspace_trust_is_actionable_not_ready_and_foreign_is_pending
419
414
  }
420
415
 
421
416
  #[test]
422
- fn contract_a_probe5_quick_start_ready_is_invalid_while_actionable_trust_remains() {
423
- use team_agent::cli::{cmd_wait_ready, CmdOutput, WaitReadyArgs};
417
+ fn contract_a_runtime_readiness_is_false_while_actionable_trust_remains() {
424
418
  use team_agent::provider::{classify_codex_startup_screen, StartupScreenDecision};
425
419
 
426
420
  assert_eq!(
@@ -429,67 +423,47 @@ fn contract_a_probe5_quick_start_ready_is_invalid_while_actionable_trust_remains
429
423
  "fixture sanity: probe5 pane text is an actionable own-workspace trust prompt"
430
424
  );
431
425
 
432
- let ws = tmp_dir("a-wait-ready-trust");
426
+ let ws = tmp_dir("a-runtime-readiness-trust");
433
427
  let mcp_config = ws.join(".team").join("runtime").join("mcp").join("w1.json");
434
428
  std::fs::create_dir_all(mcp_config.parent().unwrap()).unwrap();
435
429
  std::fs::write(&mcp_config, "{}").unwrap();
436
- team_agent::state::persist::save_runtime_state(
437
- &ws,
438
- &json!({
439
- "session_name": "team-subprep",
440
- "agents": {
441
- "w1": {
442
- "provider": "codex",
443
- "status": "awaiting_trust_prompt",
444
- "startup_prompt_status": "awaiting_trust_prompt",
445
- "startup_prompts": "awaiting_trust_prompt",
446
- "pane_id": "%7",
447
- "window": "w1",
448
- "mcp_config": mcp_config.to_string_lossy().to_string(),
449
- "task_prompt_delivered": true
450
- }
430
+ // Keep every other readiness gate satisfied; an unattached leader would
431
+ // make ready=false independently of the trust prompt under test.
432
+ let mut state = json!({
433
+ "session_name": "team-subprep",
434
+ "tmux_session_present": true,
435
+ "leader_receiver": {"status": "attached", "pane_id": "%9"},
436
+ "agents": {
437
+ "w1": {
438
+ "provider": "codex",
439
+ "status": "awaiting_trust_prompt",
440
+ "startup_prompt_status": "awaiting_trust_prompt",
441
+ "startup_prompts": "awaiting_trust_prompt",
442
+ "pane_id": "%7",
443
+ "window": "w1",
444
+ "mcp_config": mcp_config.to_string_lossy().to_string(),
445
+ "task_prompt_delivered": true
451
446
  }
452
- }),
453
- )
454
- .unwrap();
455
-
456
- let result = cmd_wait_ready(&WaitReadyArgs {
457
- workspace: ws.to_path_buf(),
458
- timeout: 0.0,
459
- json: true,
460
- team: None,
461
- })
462
- .expect("wait-ready SUT should return shaped JSON");
463
- let value = match result.output {
464
- CmdOutput::Json(value) => value,
465
- other => panic!("wait-ready --json must return JSON output, got {other:?}"),
466
- };
447
+ }
448
+ });
467
449
 
450
+ let value = crate::cli::diagnose::wait_readiness(&state);
468
451
  assert_eq!(
469
- value["ok"],
470
- json!(false),
471
- "Contract A: quick-start/wait-ready readiness must not report ok=true while actionable trust remains; got {value}"
472
- );
473
- assert_eq!(
474
- value["status"],
475
- json!("pending"),
476
- "Contract A: actionable trust prompt should keep readiness pending, not ready; got {value}"
477
- );
478
- assert_eq!(
479
- value["reason"],
480
- json!("awaiting_trust_prompt"),
481
- "Contract A: not-ready reason must identify the startup trust boundary; got {value}"
482
- );
483
- assert_eq!(
484
- value["readiness"]["awaiting_trust_prompt"],
452
+ value["awaiting_trust_prompt"],
485
453
  json!(true),
486
454
  "Contract A: ready logic must surface awaiting_trust_prompt=true; got {value}"
487
455
  );
488
456
  assert_eq!(
489
- value["readiness"]["ready"],
457
+ value["ready"],
490
458
  json!(false),
491
459
  "Contract A: ready must stay false until the actionable trust prompt is cleared; got {value}"
492
460
  );
461
+ state["agents"]["w1"]["status"] = json!("running");
462
+ state["agents"]["w1"]["startup_prompt_status"] = json!("complete");
463
+ state["agents"]["w1"]["startup_prompts"] = json!("complete");
464
+ let cleared = crate::cli::diagnose::wait_readiness(&state);
465
+ assert_eq!(cleared["awaiting_trust_prompt"], json!(false));
466
+ assert_eq!(cleared["ready"], json!(true), "positive control: {cleared}");
493
467
  }
494
468
 
495
469
  #[test]
@@ -67,7 +67,6 @@ pub mod delivery;
67
67
  pub mod helpers;
68
68
  pub mod leader_channel;
69
69
  pub mod leader_receiver;
70
- pub mod peers;
71
70
  pub mod persist;
72
71
  pub mod presentation;
73
72
  pub mod results;
@@ -96,7 +95,6 @@ pub use leader_receiver::{
96
95
  mirror_peer_message_to_leader, send_to_leader_receiver,
97
96
  send_to_leader_receiver_with_presentation,
98
97
  };
99
- pub use peers::allow_peer_talk;
100
98
  pub use persist::{
101
99
  persist_resolved_send, DeliveryBlocker, InitialDisposition, InternalSendKind,
102
100
  LogicalRecipient, PersistResolution, PersistedSend, ResolvedSendIntent, SendOrigin,
@@ -45,64 +45,6 @@ impl Default for ResultRoute {
45
45
  }
46
46
  }
47
47
 
48
- pub fn results_for_case(
49
- workspace: &Path,
50
- case_id: &str,
51
- owner_team_id: Option<&str>,
52
- requested_owner_team_id: Option<&str>,
53
- ) -> Result<Vec<serde_json::Value>, MessagingError> {
54
- let current_db = workspace.join(".team/runtime/team.db");
55
- let legacy_db = workspace.join(".team/team.db");
56
- if current_db.exists() {
57
- return results_for_case_in_db(
58
- &current_db,
59
- case_id,
60
- owner_team_id,
61
- requested_owner_team_id,
62
- );
63
- }
64
- if legacy_db.exists() {
65
- return results_for_case_in_db(&legacy_db, case_id, owner_team_id, requested_owner_team_id);
66
- }
67
- Ok(Vec::new())
68
- }
69
-
70
- fn results_for_case_in_db(
71
- db_path: &Path,
72
- case_id: &str,
73
- owner_team_id: Option<&str>,
74
- requested_owner_team_id: Option<&str>,
75
- ) -> Result<Vec<serde_json::Value>, MessagingError> {
76
- let conn = crate::db::schema::open_db(db_path)?;
77
- // The second predicate works around
78
- // `.team/bugs/BUG-caseid-writable-when-route-undeclared-20260730.md`;
79
- // it is not design intent.
80
- let case_predicate = "task_id = ?1 or json_extract(envelope,'$.presentation.case_id') = ?1";
81
- let sql = match owner_team_id {
82
- Some(_) => format!(
83
- "select envelope from results where ({case_predicate}) \
84
- and (owner_team_id = ?2 or owner_team_id = ?3) \
85
- order by created_at, result_id"
86
- ),
87
- None => format!(
88
- "select envelope from results where {case_predicate} order by created_at, result_id"
89
- ),
90
- };
91
- let mut stmt = conn.prepare(&sql)?;
92
- let row_mapper = |row: &rusqlite::Row<'_>| row.get::<_, String>(0);
93
- let rows = match (owner_team_id, requested_owner_team_id) {
94
- (Some(team), Some(requested_team)) => {
95
- stmt.query_map(params![case_id, team, requested_team], row_mapper)
96
- }
97
- (Some(team), None) => stmt.query_map(params![case_id, team, team], row_mapper),
98
- (None, _) => stmt.query_map(params![case_id], row_mapper),
99
- }?
100
- .collect::<Result<Vec<_>, _>>()?;
101
- rows.into_iter()
102
- .map(|envelope| serde_json::from_str(&envelope).map_err(MessagingError::from))
103
- .collect()
104
- }
105
-
106
48
  /// `collect` (`results.py:45`):投递 pending、捞 uncollected results、校验 envelope、更新任务态、
107
49
  /// 写 team_state、ensure coordinator。CLI `collect` + coordinator tick 调。
108
50
  pub fn collect(