@team-agent/installer 0.5.110 → 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 (54) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +231 -812
  4. package/crates/team-agent/src/cli/diagnose.rs +2 -424
  5. package/crates/team-agent/src/cli/emit.rs +493 -451
  6. package/crates/team-agent/src/cli/leader.rs +4 -9
  7. package/crates/team-agent/src/cli/leaders.rs +90 -29
  8. package/crates/team-agent/src/cli/mod.rs +348 -169
  9. package/crates/team-agent/src/cli/models.rs +80 -24
  10. package/crates/team-agent/src/cli/named_address.rs +114 -25
  11. package/crates/team-agent/src/cli/route.rs +10 -5
  12. package/crates/team-agent/src/cli/send/presentation.rs +60 -41
  13. package/crates/team-agent/src/cli/send/resolve.rs +25 -18
  14. package/crates/team-agent/src/cli/send.rs +4 -4
  15. package/crates/team-agent/src/cli/spec.rs +53 -109
  16. package/crates/team-agent/src/cli/tests/base.rs +189 -82
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +1 -2
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +2 -86
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +15 -140
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +1 -258
  21. package/crates/team-agent/src/cli/tests/mod.rs +0 -2
  22. package/crates/team-agent/src/cli/tests/named_address.rs +19 -22
  23. package/crates/team-agent/src/cli/tests/status_send.rs +15 -10
  24. package/crates/team-agent/src/cli/tests/verb_install_skill.rs +2 -2
  25. package/crates/team-agent/src/cli/types.rs +26 -121
  26. package/crates/team-agent/src/coordinator/health.rs +3 -283
  27. package/crates/team-agent/src/coordinator/mod.rs +0 -5
  28. package/crates/team-agent/src/coordinator/tests/basics.rs +0 -9
  29. package/crates/team-agent/src/coordinator/tests/mod.rs +0 -1
  30. package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -17
  31. package/crates/team-agent/src/coordinator/types.rs +1 -21
  32. package/crates/team-agent/src/db/message_store.rs +0 -42
  33. package/crates/team-agent/src/db/migration.rs +49 -9
  34. package/crates/team-agent/src/db/schema.rs +27 -20
  35. package/crates/team-agent/src/leader/owner_bind.rs +0 -24
  36. package/crates/team-agent/src/leader/registry.rs +3 -3
  37. package/crates/team-agent/src/leader/tests/byte_findings.rs +0 -20
  38. package/crates/team-agent/src/leader/tests/identity.rs +0 -32
  39. package/crates/team-agent/src/leader/tests/lease_claim.rs +0 -55
  40. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +4 -9
  41. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -51
  42. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -1
  43. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +23 -23
  44. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +29 -55
  45. package/crates/team-agent/src/messaging/mod.rs +0 -2
  46. package/crates/team-agent/src/messaging/results.rs +0 -58
  47. package/crates/team-agent/src/messaging/tests/runtime.rs +0 -46
  48. package/crates/team-agent/src/state/repository.rs +1 -5
  49. package/package.json +4 -4
  50. package/templates/team_state.md +1 -1
  51. package/crates/team-agent/src/cli/tests/peer_allow.rs +0 -164
  52. package/crates/team-agent/src/cli/tests/verb_validate.rs +0 -195
  53. package/crates/team-agent/src/coordinator/tests/watch.rs +0 -223
  54. package/crates/team-agent/src/messaging/peers.rs +0 -34
@@ -16,8 +16,16 @@ use serde_json::json;
16
16
  fn provider_args_cases() {
17
17
  // (old case-id, input, Python golden); values[1:] of a lone "--" is empty.
18
18
  const CASES: &[(&str, &[&str], &[&str])] = &[
19
- ("provider_args_strips_leading_dashdash", &["--", "-x"], &["-x"]),
20
- ("provider_args_keeps_when_no_leading_dashdash", &["-x", "-y"], &["-x", "-y"]),
19
+ (
20
+ "provider_args_strips_leading_dashdash",
21
+ &["--", "-x"],
22
+ &["-x"],
23
+ ),
24
+ (
25
+ "provider_args_keeps_when_no_leading_dashdash",
26
+ &["-x", "-y"],
27
+ &["-x", "-y"],
28
+ ),
21
29
  ("provider_args_empty_is_empty", &[], &[]),
22
30
  ("provider_args_lone_dashdash_yields_empty", &["--"], &[]),
23
31
  ];
@@ -36,41 +44,73 @@ fn provider_args_cases() {
36
44
  fn leader_launcher_args_success_cases() {
37
45
  // (old case-id, argv, complete expected structure), including default false flags.
38
46
  let cases: &[(&str, &[&str], LeaderLauncherArgs)] = &[
39
- ("leader_launcher_args_empty_all_default", &[], LeaderLauncherArgs::default()),
40
- ("leader_launcher_args_attach_and_confirm", &["--attach", "--confirm"], LeaderLauncherArgs {
41
- attach_existing: true,
42
- confirm_attach: true,
43
- ..Default::default()
44
- }),
47
+ (
48
+ "leader_launcher_args_empty_all_default",
49
+ &[],
50
+ LeaderLauncherArgs::default(),
51
+ ),
52
+ (
53
+ "leader_launcher_args_attach_and_confirm",
54
+ &["--attach", "--confirm"],
55
+ LeaderLauncherArgs {
56
+ attach_existing: true,
57
+ confirm_attach: true,
58
+ ..Default::default()
59
+ },
60
+ ),
45
61
  // --attach-existing is an alias of --attach.
46
- ("leader_launcher_args_attach_existing_alias", &["--attach-existing"], LeaderLauncherArgs {
47
- attach_existing: true,
48
- ..Default::default()
49
- }),
50
- ("leader_launcher_args_external_leader_opt_out", &["--external-leader", "--", "--model", "opus"], LeaderLauncherArgs {
51
- provider_args: vec!["--model".to_string(), "opus".to_string()],
52
- external_leader: true,
53
- ..Default::default()
54
- }),
55
- ("leader_launcher_args_attach_session_spaced", &["--attach-session", "mysess"], LeaderLauncherArgs {
56
- attach_session: Some("mysess".to_string()),
57
- ..Default::default()
58
- }),
59
- ("leader_launcher_args_attach_session_equals", &["--attach-session=mysess"], LeaderLauncherArgs {
60
- attach_session: Some("mysess".to_string()),
61
- ..Default::default()
62
- }),
62
+ (
63
+ "leader_launcher_args_attach_existing_alias",
64
+ &["--attach-existing"],
65
+ LeaderLauncherArgs {
66
+ attach_existing: true,
67
+ ..Default::default()
68
+ },
69
+ ),
70
+ (
71
+ "leader_launcher_args_external_leader_opt_out",
72
+ &["--external-leader", "--", "--model", "opus"],
73
+ LeaderLauncherArgs {
74
+ provider_args: vec!["--model".to_string(), "opus".to_string()],
75
+ external_leader: true,
76
+ ..Default::default()
77
+ },
78
+ ),
79
+ (
80
+ "leader_launcher_args_attach_session_spaced",
81
+ &["--attach-session", "mysess"],
82
+ LeaderLauncherArgs {
83
+ attach_session: Some("mysess".to_string()),
84
+ ..Default::default()
85
+ },
86
+ ),
87
+ (
88
+ "leader_launcher_args_attach_session_equals",
89
+ &["--attach-session=mysess"],
90
+ LeaderLauncherArgs {
91
+ attach_session: Some("mysess".to_string()),
92
+ ..Default::default()
93
+ },
94
+ ),
63
95
  // Known Team Agent launcher flags after `--` are rejected by the error table.
64
- ("leader_launcher_args_dashdash_passthrough_strips_separator", &["--attach", "--", "-x", "--provider-confirm"], LeaderLauncherArgs {
65
- provider_args: vec!["-x".to_string(), "--provider-confirm".to_string()],
66
- attach_existing: true,
67
- ..Default::default()
68
- }),
69
- ("leader_launcher_args_unknown_tokens_collect_as_provider_args", &["foo", "--attach", "bar"], LeaderLauncherArgs {
70
- provider_args: vec!["foo".to_string(), "bar".to_string()],
71
- attach_existing: true,
72
- ..Default::default()
73
- }),
96
+ (
97
+ "leader_launcher_args_dashdash_passthrough_strips_separator",
98
+ &["--attach", "--", "-x", "--provider-confirm"],
99
+ LeaderLauncherArgs {
100
+ provider_args: vec!["-x".to_string(), "--provider-confirm".to_string()],
101
+ attach_existing: true,
102
+ ..Default::default()
103
+ },
104
+ ),
105
+ (
106
+ "leader_launcher_args_unknown_tokens_collect_as_provider_args",
107
+ &["foo", "--attach", "bar"],
108
+ LeaderLauncherArgs {
109
+ provider_args: vec!["foo".to_string(), "bar".to_string()],
110
+ attach_existing: true,
111
+ ..Default::default()
112
+ },
113
+ ),
74
114
  ];
75
115
  for (id, input, expected) in cases {
76
116
  let argv: Vec<String> = input.iter().map(|arg| (*arg).to_string()).collect();
@@ -97,7 +137,10 @@ fn leader_launcher_args_error_cases() {
97
137
  for (id, input, expected) in CASES {
98
138
  let argv: Vec<String> = input.iter().map(|arg| (*arg).to_string()).collect();
99
139
  let err = leader_launcher_args(&argv).expect_err(id);
100
- assert!(err.to_string().contains(*expected), "{id}: unexpected error: {err}");
140
+ assert!(
141
+ err.to_string().contains(*expected),
142
+ "{id}: unexpected error: {err}"
143
+ );
101
144
  }
102
145
  }
103
146
 
@@ -110,14 +153,31 @@ fn send_target_cases() {
110
153
  // (old case-id, targets, target, complete expected enum).
111
154
  let cases: &[(&str, Option<&str>, Option<&str>, MessageTarget)] = &[
112
155
  (
113
- "send_target_fanout_strips_and_filters_empty", Some("a, b ,,c"), None,
156
+ "send_target_fanout_strips_and_filters_empty",
157
+ Some("a, b ,,c"),
158
+ None,
114
159
  MessageTarget::Fanout(vec!["a".to_string(), "b".to_string(), "c".to_string()]),
115
160
  ),
116
- ("send_target_single_target", None, Some("agent_x"), MessageTarget::Single("agent_x".to_string())),
161
+ (
162
+ "send_target_single_target",
163
+ None,
164
+ Some("agent_x"),
165
+ MessageTarget::Single("agent_x".to_string()),
166
+ ),
117
167
  // A bare "*" means broadcast to the whole team.
118
- ("send_target_broadcast_star", None, Some("*"), MessageTarget::Broadcast),
168
+ (
169
+ "send_target_broadcast_star",
170
+ None,
171
+ Some("*"),
172
+ MessageTarget::Broadcast,
173
+ ),
119
174
  // targets="" is falsy in Python, so it falls through to args.target.
120
- ("send_target_empty_targets_falls_through_to_target", Some(""), Some("fallback"), MessageTarget::Single("fallback".to_string())),
175
+ (
176
+ "send_target_empty_targets_falls_through_to_target",
177
+ Some(""),
178
+ Some("fallback"),
179
+ MessageTarget::Single("fallback".to_string()),
180
+ ),
121
181
  ];
122
182
  for (id, targets, target, expected) in cases {
123
183
  assert_eq!(send_target(*targets, *target), *expected, "{id}");
@@ -133,18 +193,52 @@ fn send_target_cases() {
133
193
  fn classify_agent_bucket_cases() {
134
194
  // (old case-id, all original (raw, health) inputs, expected bucket).
135
195
  const CASES: &[(&str, &[(&str, &str)], SummaryBucket)] = &[
136
- ("classify_failed_takes_priority", &[("failed", ""), ("error", ""), ("running", "error")], SummaryBucket::Failed),
137
- ("classify_stopped", &[("stopped", ""), ("done", ""), ("running", "done")], SummaryBucket::Stopped),
138
- ("classify_busy", &[("busy", ""), ("", "running"), ("", "working")], SummaryBucket::Busy),
196
+ (
197
+ "classify_failed_takes_priority",
198
+ &[("failed", ""), ("error", ""), ("running", "error")],
199
+ SummaryBucket::Failed,
200
+ ),
201
+ (
202
+ "classify_stopped",
203
+ &[("stopped", ""), ("done", ""), ("running", "done")],
204
+ SummaryBucket::Stopped,
205
+ ),
206
+ (
207
+ "classify_busy",
208
+ &[("busy", ""), ("", "running"), ("", "working")],
209
+ SummaryBucket::Busy,
210
+ ),
139
211
  // hstatus==idle precedes the raw==running branch.
140
- ("classify_hstatus_idle_beats_raw_running", &[("running", "idle")], SummaryBucket::Idle),
141
- ("classify_pure_running", &[("running", "")], SummaryBucket::Running),
212
+ (
213
+ "classify_hstatus_idle_beats_raw_running",
214
+ &[("running", "idle")],
215
+ SummaryBucket::Idle,
216
+ ),
217
+ (
218
+ "classify_pure_running",
219
+ &[("running", "")],
220
+ SummaryBucket::Running,
221
+ ),
142
222
  // bug-071/077/085: unmatched and blocked/stuck/missing are Unknown, NEVER idle.
143
- ("classify_blocked_and_unmatched_are_unknown_never_idle", &[("blocked", ""), ("stuck", ""), ("", "missing"), ("weird_value", ""), ("", "")], SummaryBucket::Unknown),
223
+ (
224
+ "classify_blocked_and_unmatched_are_unknown_never_idle",
225
+ &[
226
+ ("blocked", ""),
227
+ ("stuck", ""),
228
+ ("", "missing"),
229
+ ("weird_value", ""),
230
+ ("", ""),
231
+ ],
232
+ SummaryBucket::Unknown,
233
+ ),
144
234
  ];
145
235
  for (id, inputs, expected) in CASES {
146
236
  for &(raw, health) in inputs.iter() {
147
- assert_eq!(classify_agent_bucket(raw, health), *expected, "{id}: raw={raw:?}, health={health:?}");
237
+ assert_eq!(
238
+ classify_agent_bucket(raw, health),
239
+ *expected,
240
+ "{id}: raw={raw:?}, health={health:?}"
241
+ );
148
242
  }
149
243
  }
150
244
  }
@@ -395,7 +489,7 @@ fn cli_error_payload_plain_runtime() {
395
489
  assert_eq!(payload.error, "some other error");
396
490
  assert_eq!(
397
491
  payload.action,
398
- "run `team-agent doctor` or inspect the log path shown here"
492
+ "先运行 team-agent doctor --workspace . 检查所选队伍,或查看此处列出的错误日志。"
399
493
  );
400
494
  assert_eq!(payload.log, "/tmp/y.log");
401
495
  assert_eq!(payload.reason, None);
@@ -417,19 +511,14 @@ fn cli_error_payload_tmux_conflict_quick_start_enrichment() {
417
511
  // context reset is only through restart --allow-fresh with explicit consent.
418
512
  assert_eq!(
419
513
  payload.action,
420
- "tmux session `my-team` already exists. It may be your own existing team. \
421
- To resume it use `team-agent restart`. \
422
- If recovery is impossible, use `team-agent restart --allow-fresh` only after explicit context-loss consent. \
423
- Only if you want a separate team, change `name:` in TEAM.md and run quick-start again. \
424
- Never terminate existing tmux sessions from quick-start."
425
- );
426
- assert_eq!(
427
- payload.next_actions,
428
- Some(vec![
429
- "If this is your existing team, resume it with `team-agent restart`.".to_string(),
430
- "If you want a separate team, change `name:` in TEAM.md and run `team-agent quick-start` again.".to_string(),
431
- ])
514
+ "终端会话 `my-team` 已存在,可能是你的已有队伍。恢复用 team-agent restart;只有明确同意丢弃旧对话才用 --allow-fresh。另建队伍请修改 TEAM.md 的 name 后再 quick-start;不要关闭已有队伍凑成功。"
432
515
  );
516
+ let next = payload.next_actions.as_ref().unwrap();
517
+ assert_eq!(next.len(), 2);
518
+ assert!(next[0].contains("team-agent restart"));
519
+ assert!(
520
+ next[1].contains("TEAM.md") && next[1].contains("name") && next[1].contains("quick-start")
521
+ );
433
522
  }
434
523
 
435
524
  #[test]
@@ -440,16 +529,11 @@ fn cli_error_payload_tmux_conflict_non_quick_start_enrichment() {
440
529
  assert_eq!(payload.session_name.as_deref(), Some("my-team"));
441
530
  assert_eq!(
442
531
  payload.action,
443
- "tmux session `my-team` already exists. It may be an active team. \
444
- Do not terminate existing tmux sessions from startup; \
445
- use a different team name or runtime.session_name and start again."
446
- );
447
- assert_eq!(
448
- payload.next_actions,
449
- Some(vec![
450
- "Use a different team name or runtime.session_name before starting again.".to_string()
451
- ])
532
+ "终端会话 `my-team` 已存在,可能属于运行中的队伍。不要关闭它;请使用另一个队伍名称后再启动。"
452
533
  );
534
+ let next = payload.next_actions.as_ref().unwrap();
535
+ assert_eq!(next.len(), 1);
536
+ assert!(next[0].contains("队伍") && next[0].contains("名称"));
453
537
  }
454
538
 
455
539
  #[test]
@@ -641,19 +725,42 @@ fn cmd_status_summary_with_agent_rejected() {
641
725
  // =========================================================================
642
726
 
643
727
  #[test]
644
- fn cmd_leader_passthrough_help_returns_none() {
645
- // parser.py:516: provider_args in (["-h"],["--help"]) -> print usage, return (no emit).
646
- let r = cmd_leader_passthrough("codex", &["-h".into()], Path::new(".")).unwrap();
647
- assert_eq!(r.output, CmdOutput::None);
648
- assert_eq!(r.exit, ExitCode::Ok);
649
- let r2 = cmd_leader_passthrough("claude", &["--help".into()], Path::new(".")).unwrap();
650
- assert_eq!(r2.output, CmdOutput::None);
651
- let r3 = cmd_leader_passthrough("copilot", &["--help".into()], Path::new(".")).unwrap();
652
- assert_eq!(r3.output, CmdOutput::None);
653
- let r4 = cmd_leader_passthrough("grok", &["-h".into()], Path::new(".")).unwrap();
654
- assert_eq!(r4.output, CmdOutput::None);
655
- let r5 = cmd_leader_passthrough("cursor", &["--help".into()], Path::new(".")).unwrap();
656
- assert_eq!(r5.output, CmdOutput::None);
728
+ #[serial_test::serial(env)]
729
+ fn cmd_leader_passthrough_help_is_pure_human_guidance() {
730
+ let env = crate::cli::hermetic_test_support::HermeticTestEnv::enter("launcher-help-unit");
731
+ let cwd = env.workspace("empty");
732
+ let tools = [
733
+ "pi", "codex", "claude", "copilot", "grok", "agent", "cursor",
734
+ ];
735
+ let bin = env.root().join("canaries");
736
+ std::fs::create_dir(&bin).unwrap();
737
+ #[cfg(unix)]
738
+ let _path = {
739
+ use std::os::unix::fs::PermissionsExt;
740
+ for tool in tools {
741
+ let path = bin.join(tool);
742
+ std::fs::write(&path, "#!/bin/sh\nprintf called > \"$0.called\"\nexit 0\n").unwrap();
743
+ std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).unwrap();
744
+ }
745
+ env.with_env("PATH", &bin.to_string_lossy())
746
+ };
747
+ for provider in ["pi", "codex", "claude", "copilot", "grok", "cursor"] {
748
+ for alias in ["-h", "--help"] {
749
+ let result = cmd_leader_passthrough(provider, &[alias.into()], &cwd).unwrap();
750
+ assert_eq!(result.exit, ExitCode::Ok);
751
+ assert!(
752
+ matches!(&result.output, CmdOutput::Human(text) if text.contains(provider) && text.contains("用法") && text.contains("Examples") && text.contains("下一步"))
753
+ );
754
+ }
755
+ }
756
+ assert_eq!(std::fs::read_dir(&cwd).unwrap().count(), 0);
757
+ assert!(env.registry_entries().is_empty());
758
+ for tool in tools {
759
+ assert!(
760
+ !bin.join(format!("{tool}.called")).exists(),
761
+ "native tool invoked"
762
+ );
763
+ }
657
764
  }
658
765
 
659
766
  #[test]
@@ -257,8 +257,7 @@ fn inbox_excludes_leader_stage_result_notifications() {
257
257
  // the deterministic check sub-shapes (run_id is a random uuid; not value-locked). ────────────────
258
258
  #[test]
259
259
  fn comms_selftest_golden_boundary_scope_and_check_shapes() {
260
- let boundary = "validates live pane binding consistency and zero-token comms contracts. \
261
- Does NOT perform live runtime message round-trip. (zero token, zero pollution)";
260
+ let boundary = "这里只检查终端连接是否一致,不实际发送消息,也不证明队友已回复。";
262
261
  let ws = tmp_workspace();
263
262
  let v = diagnose_port::comms_selftest(&ws, None, None).expect("comms_selftest");
264
263
  let obj = v.as_object().expect("comms dict");
@@ -100,11 +100,10 @@ fn cli_shutdown_kills_team_session_real_teardown() {
100
100
 
101
101
  // =========================================================================
102
102
  // WAVE-2 Lane B — CLI leader handler delegation byte-parity (leader_port::*).
103
- // The three CLI verbs are thin pass-throughs (cli/commands.py:152-161):
103
+ // The retained CLI verbs are thin pass-throughs (cli/commands.py:152-157):
104
104
  // cmd_takeover -> runtime.takeover(ws, team, confirm)
105
105
  // cmd_claim_leader -> runtime.claim_leader(ws, team, confirm) (Family A)
106
- // cmd_identity -> runtime.leader_identity(ws, team)
107
- // leader_port::{takeover,claim_leader,leader_identity} are STUBS returning
106
+ // leader_port::{takeover,claim_leader} are STUBS returning
108
107
  // the WRONG shape today -> these LOCK the golden dict so the porter wires
109
108
  // them into leader::* / runtime.* and matches byte-for-byte.
110
109
  // Golden re-probed @ team-agent-public (probe_claim.py / probe_rtclaim.py /
@@ -297,89 +296,6 @@ fn leader_port_claim_leader_no_pane_refuses_caller_pane_missing_family_a() {
297
296
  );
298
297
  }
299
298
 
300
- // RED — leader_identity(): CLI directly emits leader.leader_identity's 9-key
301
- // dict (runtime.leader_identity is imported from leader). golden probe_lid.py
302
- // keys: ok, uuid_prefix, machine_fingerprint, workspace_abspath, os_user,
303
- // team_id, current_pane_id, last_seen_at, source. Current stub returns
304
- // {ok:true, team:...} (wrong shape) -> RED.
305
- #[test]
306
- fn leader_port_leader_identity_emits_nine_key_dict() {
307
- let ws = leader_port_ws("identity");
308
- std::fs::create_dir_all(crate::model::paths::runtime_dir(&ws)).unwrap();
309
- let v = super::leader_port::leader_identity(&ws, None).unwrap();
310
- assert_eq!(v["ok"], json!(true));
311
- let obj = v.as_object().expect("identity → JSON object");
312
- for key in [
313
- "ok",
314
- "uuid_prefix",
315
- "machine_fingerprint",
316
- "workspace_abspath",
317
- "os_user",
318
- "team_id",
319
- "current_pane_id",
320
- "last_seen_at",
321
- "source",
322
- ] {
323
- assert!(
324
- obj.contains_key(key),
325
- "golden identity dict must carry '{key}', got {obj:?}"
326
- );
327
- }
328
- // no override/state uuid → source is the leader-plan "derived" string.
329
- assert_eq!(v["source"], json!("derived"));
330
- // uuid_prefix is exactly 12 hex chars (derive[:12]).
331
- let prefix = v["uuid_prefix"].as_str().expect("uuid_prefix str");
332
- assert_eq!(prefix.len(), 12, "uuid_prefix == derived[:12]");
333
- assert!(prefix.chars().all(|c| c.is_ascii_hexdigit()));
334
- // no team registered + no TMUX_PANE/receiver → these are JSON null.
335
- if std::env::var_os("TMUX_PANE").is_none() {
336
- assert_eq!(v["current_pane_id"], serde_json::Value::Null);
337
- }
338
- assert_eq!(v["last_seen_at"], serde_json::Value::Null);
339
- }
340
-
341
- // ── cmd_watch (cli/adapters.rs:58) [RED] — today a CmdResult::none() no-op ───────────────────────
342
- // Golden cmd_watch (cli/commands.py:103-109) DELEGATES to run_watch(workspace.resolve(), team) and
343
- // exits 0. Golden run_watch (watch/__init__.py:25-37) is a `while True` LIVE TAIL that streams
344
- // render_event_line output (collect_watch_lines) for the watched team. The Rust cmd_watch returns
345
- // CmdResult::none() — a no-op that never touches the watch subsystem. The watch LINE SHAPE is itself
346
- // byte-locked by coordinator::render_event_line (coordinator/tests.rs GROUP H); the cli contract here
347
- // is that cmd_watch must DELEGATE and surface those rendered lines. Seeded a result_received event ->
348
- // golden render = "result_received: <agent> -> <summary>" (render_event_line: agent_id + summary[:80]).
349
- //
350
- // RED confirmed TODAY: cmd_watch=none() returns immediately (no watch line) -> the assertion fails
351
- // (and does NOT hang). PORTER: wire cmd_watch -> coordinator::run_watch(resolved workspace, team,
352
- // interval, sink). Since golden's tail is `while True`, the cli port needs a TERMINATING / bounded
353
- // entry to be both byte-parity AND unit-testable (collect the current watch lines into the CmdResult,
354
- // or stream to stdout with a bounded test seam) — a blocking unit test is unacceptable.
355
- #[test]
356
- fn cmd_watch_delegates_and_surfaces_rendered_watch_lines_not_noop() {
357
- let ws = tmp_workspace();
358
- let logs = crate::model::paths::logs_dir(&ws);
359
- std::fs::create_dir_all(&logs).unwrap();
360
- std::fs::write(
361
- logs.join("events.jsonl"),
362
- "{\"event\":\"result_received\",\"agent_id\":\"alpha\",\"summary\":\"did the thing\"}\n",
363
- )
364
- .unwrap();
365
- let r = cmd_watch(&WatchArgs {
366
- workspace: ws.clone(),
367
- team: None,
368
- })
369
- .expect("cmd_watch returns a CmdResult");
370
- let text = match &r.output {
371
- CmdOutput::Human(s) => s.clone(),
372
- CmdOutput::Json(v) => v.to_string(),
373
- CmdOutput::None => String::new(),
374
- };
375
- assert!(
376
- text.contains("result_received: alpha -> did the thing"),
377
- "cmd_watch must DELEGATE to the watch subsystem (run_watch -> render_event_line) and surface the \
378
- rendered watch line; today it is a CmdResult::none() no-op. got output={:?}",
379
- r.output
380
- );
381
- }
382
-
383
299
  // D7 [WARN] — WAVE-2 Lane B: empty caller binds refuse as caller_pane_missing.
384
300
  #[test]
385
301
  #[serial_test::serial(env)]
@@ -175,58 +175,7 @@ fn inbox_matches_sender_or_recipient_and_excludes_others() {
175
175
  );
176
176
  let _ = std::fs::remove_dir_all(&ws);
177
177
  }
178
- // ── BUG-4 [real bug] — peek must resolve the agent terminal via state `session:window` (golden),
179
- // NOT a stored `pane_id` field. A live worker present in state (with a `window`, on the team's `-L`
180
- // socket) must NOT be fabricated as "agent pane not found". ──────────────────────────────────────
181
- // Golden status/peek.py:35-44: agent = state["agents"][id]; window = agent.get("window", id);
182
- // if not session_name or not _tmux_window_exists(session_name, window): raise "agent terminal is
183
- // not available: <id>"; else `tmux capture-pane -t session:window`. It NEVER reads a stored pane_id.
184
- // Probed live (/tmp/probe_peek.py): a present worker whose window is absent on the socket raises
185
- // `agent terminal is not available: w1`; a missing agent raises `unknown agent id: <id>`. Rust
186
- // cmd_peek (adapters.rs:231 + agent_pane_id:279) keys off agent_state pane_id/pane/tmux_pane_id and
187
- // returns {ok:false,error:"agent pane not found"} when absent — so a NORMAL live worker (window in
188
- // state, no pane_id field) is mis-reported as not found. That is the CP-1 pane-resolution divergence.
189
- //
190
- // Deterministic without real tmux: on a host with no live session, the golden-correct peek resolves
191
- // session:window, finds the window absent on the socket, and yields "agent terminal is not available:
192
- // w1" — NOT "agent pane not found". (The window-on-socket -> real raw-screen capture positive case is
193
- // real-machine; see the #[ignore] dispatch_routes_peek_real_machine.)
194
- #[test]
195
- fn peek_resolves_live_worker_via_session_window_not_pane_id_field() {
196
- let ws = tmp_workspace();
197
- // a live worker: present in state with a `window`, session_name set, but NO stored pane_id field.
198
- // session name is unique so a real tmux session on the dev host can't accidentally satisfy it.
199
- crate::state::persist::save_runtime_state(
200
- &ws,
201
- &json!({
202
- "session_name": "team-peek-red-probe-x9q",
203
- "agents": {"w1": {"status": "running", "provider": "codex", "window": "w1"}}
204
- }),
205
- )
206
- .unwrap();
207
- let args = PeekArgs {
208
- agent: "w1".to_string(),
209
- workspace: ws.clone(),
210
- tail: 20,
211
- head: None,
212
- search: None,
213
- allow_raw_screen: true,
214
- json: true,
215
- };
216
- let text = outcome_text(cmd_peek(&args));
217
- assert!(
218
- !text.contains("agent pane not found"),
219
- "peek keys off a stored pane_id field and fabricates 'agent pane not found' for a live worker \
220
- that has a `window` in state; golden resolves session:window and never reads pane_id. got: {text}"
221
- );
222
- assert!(
223
- text.contains("agent terminal is not available: w1"),
224
- "golden status/peek.py: a worker whose window is not on the socket yields \
225
- 'agent terminal is not available: w1' (window-existence via session:window), NOT a \
226
- pane_id-keyed error. got: {text}"
227
- );
228
- let _ = std::fs::remove_dir_all(&ws);
229
- }
178
+
230
179
  #[test]
231
180
  fn ux_doctor_secret_scan_is_present_and_non_triggering_for_normal_paths() {
232
181
  let ws = tmp_workspace();
@@ -295,44 +244,16 @@ fn ux_doctor_secret_scan_findings_name_the_exact_trigger() {
295
244
  let _ = std::fs::remove_dir_all(&ws);
296
245
  }
297
246
  #[test]
298
- fn ux_wait_ready_does_not_report_ready_true_without_ready_runtime_state() {
299
- let ws = tmp_workspace();
300
- crate::state::persist::save_runtime_state(
301
- &ws,
302
- &json!({
303
- "active_team_key": "wait-ready",
304
- "agents": {"w1": {"status": "starting"}},
305
- "tasks": [{"id": "t1", "assignee": "w1", "status": "pending"}],
306
- "leader_receiver": {"status": "attached"},
307
- }),
308
- )
309
- .unwrap();
310
- let value = json_output(
311
- cmd_wait_ready(&WaitReadyArgs {
312
- workspace: ws.clone(),
313
- timeout: 0.0,
314
- json: true,
315
- team: None,
316
- })
317
- .expect("wait-ready"),
318
- );
319
- assert_eq!(
320
- value["ok"],
321
- json!(false),
322
- "wait-ready must not fake success before workers are ready"
323
- );
324
- assert_eq!(value.pointer("/readiness/ready"), Some(&json!(false)));
325
- assert!(
326
- value["summary"]
327
- .as_str()
328
- .unwrap_or("")
329
- .contains("not ready"),
330
- "wait-ready false state should explain not-ready status, got {value:?}"
331
- );
332
- let _ = std::fs::remove_dir_all(&ws);
247
+ fn runtime_readiness_rejects_starting_workers() {
248
+ let value = crate::cli::diagnose::wait_readiness(&json!({
249
+ "agents": {"w1": {"status": "starting"}},
250
+ "leader_receiver": {"status": "attached"},
251
+ }));
252
+ assert_eq!(value["ready"], json!(false));
253
+ assert_eq!(value["all_spawned"], json!(false));
333
254
  }
334
255
  #[test]
335
- fn wait_ready_fake_quick_start_counts_mcp_config_and_task_prompt_delivery() {
256
+ fn runtime_readiness_counts_mcp_config_and_task_prompt_delivery() {
336
257
  let ws = tmp_workspace();
337
258
  let mcp_config = ws
338
259
  .join(".team")
@@ -342,10 +263,9 @@ fn wait_ready_fake_quick_start_counts_mcp_config_and_task_prompt_delivery() {
342
263
  .join("mcp_config.json");
343
264
  std::fs::create_dir_all(mcp_config.parent().unwrap()).unwrap();
344
265
  std::fs::write(&mcp_config, r#"{"mcpServers":{"team-agent":{}}}"#).unwrap();
345
- crate::state::persist::save_runtime_state(
346
- &ws,
347
- &json!({
266
+ let state = json!({
348
267
  "session_name": "team-fake-ready",
268
+ "messages": {"delivered": 1},
349
269
  "agents": {
350
270
  "fake_impl": {
351
271
  "status": "running",
@@ -359,30 +279,9 @@ fn wait_ready_fake_quick_start_counts_mcp_config_and_task_prompt_delivery() {
359
279
  "status": "pending",
360
280
  }],
361
281
  "leader_receiver": {"status": "attached"},
362
- }),
363
- )
364
- .unwrap();
365
- let store = crate::message_store::MessageStore::open(&ws).unwrap();
366
- store
367
- .create_message(
368
- Some("task_impl"),
369
- "leader",
370
- "fake_impl",
371
- "initial task prompt",
372
- None,
373
- true,
374
- None,
375
- )
376
- .unwrap();
377
- let value = json_output(
378
- cmd_wait_ready(&WaitReadyArgs {
379
- workspace: ws.clone(),
380
- timeout: 0.0,
381
- json: true,
382
- team: None,
383
- })
384
- .expect("wait-ready"),
385
- );
282
+ });
283
+ let readiness = crate::cli::diagnose::wait_readiness(&state);
284
+ let value = json!({"readiness": readiness});
386
285
  assert_eq!(
387
286
  value.pointer("/readiness/mcp_ready"),
388
287
  Some(&json!(true)),
@@ -400,31 +299,7 @@ fn wait_ready_fake_quick_start_counts_mcp_config_and_task_prompt_delivery() {
400
299
  );
401
300
  let _ = std::fs::remove_dir_all(&ws);
402
301
  }
403
- fn valid_result_envelope() -> serde_json::Value {
404
- json!({
405
- "schema_version": "result_envelope_v1",
406
- "task_id": "task_impl",
407
- "agent_id": "fake_impl",
408
- "status": "success",
409
- "summary": "done",
410
- "artifacts": [],
411
- "changes": [],
412
- "tests": [{"command": "cargo test", "status": "passed"}],
413
- "risks": [],
414
- "next_actions": []
415
- })
416
- }
417
- fn seed_uncollected_result(ws: &std::path::Path, result_id: &str) {
418
- let store = crate::message_store::MessageStore::open(ws).unwrap();
419
- let conn = crate::db::schema::open_db(store.db_path()).unwrap();
420
- conn.execute(
421
- "insert into results(
422
- result_id, owner_team_id, task_id, agent_id, envelope, status, created_at
423
- ) values (?1, null, 'task_impl', 'fake_impl', ?2, 'success', '2026-06-02T10:00:00+00:00')",
424
- rusqlite::params![result_id, valid_result_envelope().to_string()],
425
- )
426
- .unwrap();
427
- }
302
+
428
303
  fn json_output(result: CmdResult) -> serde_json::Value {
429
304
  match result.output {
430
305
  CmdOutput::Json(v) => v,