@team-agent/installer 0.5.66 → 0.5.68

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 (164) hide show
  1. package/Cargo.lock +8 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +1 -0
  4. package/crates/team-agent/src/cli/adapters.rs +5 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +82 -0
  6. package/crates/team-agent/src/cli/emit.rs +3 -3
  7. package/crates/team-agent/src/cli/grok_slot.rs +299 -0
  8. package/crates/team-agent/src/cli/leader.rs +21 -7
  9. package/crates/team-agent/src/cli/leaders.rs +125 -1
  10. package/crates/team-agent/src/cli/mod.rs +20 -0
  11. package/crates/team-agent/src/cli/send/presentation.rs +7 -1
  12. package/crates/team-agent/src/cli/spec.rs +2 -0
  13. package/crates/team-agent/src/cli/status_port/compact.rs +28 -0
  14. package/crates/team-agent/src/cli/status_port/snapshot.rs +25 -1
  15. package/crates/team-agent/src/cli/tests/base.rs +26 -3
  16. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +129 -19
  17. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +387 -6
  18. package/crates/team-agent/src/cli/tests/status_send.rs +79 -10
  19. package/crates/team-agent/src/communication_mode/mod.rs +6 -4
  20. package/crates/team-agent/src/compiler.rs +59 -0
  21. package/crates/team-agent/src/coordinator/backoff.rs +55 -0
  22. package/crates/team-agent/src/coordinator/conpty_shim.rs +82 -0
  23. package/crates/team-agent/src/coordinator/health.rs +173 -0
  24. package/crates/team-agent/src/coordinator/mod.rs +31 -0
  25. package/crates/team-agent/src/coordinator/orphan.rs +31 -0
  26. package/crates/team-agent/src/coordinator/runtime_detectors.rs +30 -0
  27. package/crates/team-agent/src/coordinator/runtime_observation.rs +25 -0
  28. package/crates/team-agent/src/coordinator/steps/abnormal.rs +59 -0
  29. package/crates/team-agent/src/coordinator/steps/delivery.rs +10 -0
  30. package/crates/team-agent/src/coordinator/steps/health_sync.rs +10 -0
  31. package/crates/team-agent/src/coordinator/steps/mod.rs +22 -0
  32. package/crates/team-agent/src/coordinator/steps/persist.rs +10 -0
  33. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +10 -0
  34. package/crates/team-agent/src/coordinator/steps/session_gate.rs +10 -0
  35. package/crates/team-agent/src/coordinator/tick.rs +133 -22
  36. package/crates/team-agent/src/coordinator/types.rs +49 -0
  37. package/crates/team-agent/src/db/message_store.rs +39 -5
  38. package/crates/team-agent/src/layout/worker_env.rs +20 -3
  39. package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
  40. package/crates/team-agent/src/leader/provider_attribution.rs +53 -0
  41. package/crates/team-agent/src/leader/registry.rs +65 -0
  42. package/crates/team-agent/src/leader/start.rs +920 -63
  43. package/crates/team-agent/src/lifecycle/display.rs +56 -0
  44. package/crates/team-agent/src/lifecycle/helpers.rs +57 -0
  45. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +332 -12
  46. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +62 -0
  47. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +59 -0
  48. package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +101 -11
  49. package/crates/team-agent/src/lifecycle/launch/cursor_create_chat.rs +229 -0
  50. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +332 -0
  51. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +204 -456
  52. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +24 -0
  53. package/crates/team-agent/src/lifecycle/launch/grok_per_seat.rs +260 -0
  54. package/crates/team-agent/src/lifecycle/launch/identity.rs +146 -0
  55. package/crates/team-agent/src/lifecycle/launch/layout.rs +60 -0
  56. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +112 -0
  57. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +530 -0
  58. package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -0
  59. package/crates/team-agent/src/lifecycle/launch/plan.rs +31 -0
  60. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +66 -0
  61. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +78 -0
  62. package/crates/team-agent/src/lifecycle/launch/readiness.rs +85 -38
  63. package/crates/team-agent/src/lifecycle/launch/role_source.rs +44 -44
  64. package/crates/team-agent/src/lifecycle/launch/spawn.rs +62 -2
  65. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +139 -0
  66. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +109 -0
  67. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +214 -1
  68. package/crates/team-agent/src/lifecycle/launch.rs +99 -29
  69. package/crates/team-agent/src/lifecycle/lock.rs +42 -1
  70. package/crates/team-agent/src/lifecycle/mod.rs +11 -0
  71. package/crates/team-agent/src/lifecycle/pane_input_lock.rs +161 -0
  72. package/crates/team-agent/src/lifecycle/profile_launch.rs +98 -0
  73. package/crates/team-agent/src/lifecycle/profile_smoke.rs +51 -1
  74. package/crates/team-agent/src/lifecycle/restart/agent.rs +124 -0
  75. package/crates/team-agent/src/lifecycle/restart/common.rs +369 -11
  76. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +27 -0
  77. package/crates/team-agent/src/lifecycle/restart/preflight.rs +25 -0
  78. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +106 -0
  79. package/crates/team-agent/src/lifecycle/restart/remove.rs +196 -5
  80. package/crates/team-agent/src/lifecycle/restart/selection.rs +72 -0
  81. package/crates/team-agent/src/lifecycle/restart/team_state.rs +22 -0
  82. package/crates/team-agent/src/lifecycle/restart.rs +29 -0
  83. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +2 -3
  84. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +309 -0
  85. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +150 -157
  86. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +2 -2
  87. package/crates/team-agent/src/lifecycle/tests/core.rs +4 -0
  88. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +287 -0
  89. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +229 -0
  90. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
  91. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +784 -0
  92. package/crates/team-agent/src/lifecycle/tests/gate_fixtures.rs +562 -0
  93. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +239 -0
  94. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +498 -0
  95. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +250 -0
  96. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +293 -0
  97. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +63 -38
  98. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +3 -2
  99. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +16 -8
  100. package/crates/team-agent/src/lifecycle/tests/mcp_tool_name_format_red.rs +69 -0
  101. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +20 -19
  102. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +97 -61
  103. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +2 -0
  104. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +40 -7
  105. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +59 -1
  106. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +69 -26
  107. package/crates/team-agent/src/lifecycle/tests.rs +23 -13
  108. package/crates/team-agent/src/lifecycle/types.rs +61 -0
  109. package/crates/team-agent/src/lifecycle/worker_command_context.rs +85 -11
  110. package/crates/team-agent/src/mcp_server/tests/scoped.rs +100 -1
  111. package/crates/team-agent/src/mcp_server/tests.rs +196 -2
  112. package/crates/team-agent/src/messaging/delivery.rs +405 -40
  113. package/crates/team-agent/src/messaging/helpers.rs +2 -0
  114. package/crates/team-agent/src/messaging/leader_receiver.rs +53 -1
  115. package/crates/team-agent/src/messaging/results.rs +4 -0
  116. package/crates/team-agent/src/messaging/send.rs +34 -0
  117. package/crates/team-agent/src/messaging/tests/dup_inject.rs +406 -0
  118. package/crates/team-agent/src/messaging/tests/e23.rs +9 -0
  119. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +66 -0
  120. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  121. package/crates/team-agent/src/messaging/types.rs +4 -1
  122. package/crates/team-agent/src/model/enums.rs +14 -5
  123. package/crates/team-agent/src/model/permissions.rs +3 -0
  124. package/crates/team-agent/src/os_probe.rs +73 -2
  125. package/crates/team-agent/src/provider/adapter.rs +231 -4
  126. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +86 -0
  127. package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
  128. package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
  129. package/crates/team-agent/src/provider/bypass_flags.rs +46 -2
  130. package/crates/team-agent/src/provider/classify.rs +4 -2
  131. package/crates/team-agent/src/provider/faults.rs +2 -1
  132. package/crates/team-agent/src/provider/mod.rs +11 -0
  133. package/crates/team-agent/src/provider/session/capture.rs +158 -20
  134. package/crates/team-agent/src/provider/session/context_fork/claude.rs +38 -1
  135. package/crates/team-agent/src/provider/session/context_fork/codex.rs +72 -1
  136. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +57 -1
  137. package/crates/team-agent/src/provider/session/context_fork.rs +77 -3
  138. package/crates/team-agent/src/provider/session/mod.rs +18 -0
  139. package/crates/team-agent/src/provider/session/resume.rs +57 -0
  140. package/crates/team-agent/src/provider/session_scan/claude.rs +125 -1
  141. package/crates/team-agent/src/provider/session_scan/codex.rs +94 -1
  142. package/crates/team-agent/src/provider/session_scan/common.rs +204 -2
  143. package/crates/team-agent/src/provider/session_scan/copilot.rs +33 -1
  144. package/crates/team-agent/src/provider/session_scan/cursor.rs +538 -0
  145. package/crates/team-agent/src/provider/session_scan/grok.rs +288 -0
  146. package/crates/team-agent/src/provider/session_scan.rs +8 -0
  147. package/crates/team-agent/src/provider/submit_now.rs +94 -0
  148. package/crates/team-agent/src/provider/tests/adapter.rs +304 -0
  149. package/crates/team-agent/src/provider/types.rs +4 -2
  150. package/crates/team-agent/src/provider/wire.rs +23 -1
  151. package/crates/team-agent/src/state/persist.rs +301 -8
  152. package/crates/team-agent/src/state/repository.rs +5 -0
  153. package/crates/team-agent/src/tmux_backend/tests.rs +1737 -42
  154. package/crates/team-agent/src/tmux_backend.rs +1064 -75
  155. package/crates/team-agent/src/transport/tests/wire.rs +28 -2
  156. package/crates/team-agent/src/transport.rs +248 -1
  157. package/npm/install.mjs +129 -73
  158. package/package.json +4 -4
  159. package/skills/team-agent/SKILL.md +33 -238
  160. package/skills/team-agent/command-coverage.json +31 -0
  161. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +0 -59
  162. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +0 -488
  163. package/crates/team-agent/src/lifecycle/launch/fork_pending.rs +0 -109
  164. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +0 -447
@@ -468,12 +468,52 @@ fn scoped_degraded_shutdown_does_not_persist_team_shutdown_status() {
468
468
 
469
469
  #[test]
470
470
  fn repeated_owned_endpoint_shutdowns_leave_no_socket_file_growth() {
471
+ #[cfg(unix)]
472
+ const CHILD: &str = "RB11_F01_NATURAL_PROBE_FAILURE_CHILD";
473
+ #[cfg(unix)]
474
+ const TEST: &str =
475
+ "cli::tests::shutdown_kill_plan::repeated_owned_endpoint_shutdowns_leave_no_socket_file_growth";
476
+
477
+ #[cfg(unix)]
478
+ if std::env::var_os(CHILD).is_none() {
479
+ let path = tmp_shutdown_workspace("rb11-f01-empty-path").join("bin");
480
+ std::fs::create_dir_all(&path).unwrap();
481
+ let output = std::process::Command::new(std::env::current_exe().unwrap())
482
+ .args(["--exact", TEST, "--nocapture", "--test-threads=1"])
483
+ .env(CHILD, "1")
484
+ .env("PATH", path)
485
+ .output()
486
+ .unwrap();
487
+ assert!(
488
+ output.status.success(),
489
+ "directed child failed: status={} stdout={} stderr={}",
490
+ output.status,
491
+ String::from_utf8_lossy(&output.stdout),
492
+ String::from_utf8_lossy(&output.stderr)
493
+ );
494
+ let stdout = String::from_utf8_lossy(&output.stdout);
495
+ assert!(
496
+ stdout.contains("running 1 test")
497
+ && stdout.contains(&format!("test {TEST} ..."))
498
+ && stdout.contains("RB11_F01_CHILD_SUCCESS"),
499
+ "directed child omitted success marker: stdout={} stderr={}",
500
+ stdout,
501
+ String::from_utf8_lossy(&output.stderr)
502
+ );
503
+ println!("RB11_F01_DIRECTED_CHILD_OUTPUT\n{stdout}");
504
+ return;
505
+ }
506
+
507
+ #[cfg(unix)]
508
+ let expected_probe_failure = std::env::var_os(CHILD).is_some();
509
+ #[cfg(not(unix))]
510
+ let expected_probe_failure = false;
471
511
  let ws = tmp_shutdown_workspace("owned-loop-no-growth");
472
512
  let sockets = (0..20)
473
513
  .map(|idx| ws.join(format!("owned-loop-{idx}.sock")))
474
514
  .collect::<Vec<_>>();
475
515
  let starting = sockets.iter().filter(|path| path.exists()).count();
476
- for socket in &sockets {
516
+ for (iteration, socket) in sockets.iter().enumerate() {
477
517
  std::fs::write(socket, b"socket").unwrap();
478
518
  crate::state::persist::save_runtime_state(
479
519
  &ws,
@@ -487,20 +527,251 @@ fn repeated_owned_endpoint_shutdowns_leave_no_socket_file_growth() {
487
527
  }),
488
528
  )
489
529
  .unwrap();
490
- let out = crate::cli::lifecycle_port::shutdown_with_transport(
530
+ let before_len = crate::event_log::EventLog::new(&ws)
531
+ .tail(0)
532
+ .expect("read shutdown event boundary")
533
+ .len();
534
+ let result = crate::cli::lifecycle_port::shutdown_with_transport(
491
535
  &ws,
492
536
  true,
493
537
  None,
494
538
  &CleanShutdownTransport::new(),
495
- )
496
- .expect("shutdown should complete");
497
- assert_eq!(out["ok"], json!(true), "shutdown report: {out}");
539
+ );
540
+ let out = match &result {
541
+ Ok(out) if !shutdown_result_is_degraded(out) => out,
542
+ Ok(out) if expected_probe_failure => {
543
+ let diagnostic =
544
+ shutdown_invocation_diagnostic(&ws, before_len, iteration, socket, &result);
545
+ assert!(
546
+ shutdown_result_is_degraded(out),
547
+ "directed probe-failure child unexpectedly clean: {diagnostic}"
548
+ );
549
+ assert_eq!(
550
+ out["ok"],
551
+ json!(false),
552
+ "probe failure must remain degraded: {out}"
553
+ );
554
+ if iteration == 0 {
555
+ println!("RB11_F01_FIRST_FAILURE {diagnostic}");
556
+ }
557
+ out
558
+ }
559
+ Ok(out) => {
560
+ let diagnostic =
561
+ shutdown_invocation_diagnostic(&ws, before_len, iteration, socket, &result);
562
+ panic!("unexpected degraded shutdown: {diagnostic}");
563
+ }
564
+ Err(error) => {
565
+ let diagnostic =
566
+ shutdown_invocation_diagnostic(&ws, before_len, iteration, socket, &result);
567
+ panic!("shutdown invocation failed: {error}; {diagnostic}");
568
+ }
569
+ };
570
+ if !expected_probe_failure {
571
+ assert_eq!(out["ok"], json!(true), "shutdown report: {out}");
572
+ }
498
573
  }
499
574
  let ending = sockets.iter().filter(|path| path.exists()).count();
500
575
  assert_eq!(
501
576
  ending, starting,
502
577
  "owned socket files must not grow across loops"
503
578
  );
579
+ #[cfg(unix)]
580
+ if expected_probe_failure {
581
+ println!("RB11_F01_CHILD_SUCCESS iterations={}", sockets.len());
582
+ }
583
+ }
584
+
585
+ fn shutdown_result_is_degraded(out: &serde_json::Value) -> bool {
586
+ out["ok"] != json!(true)
587
+ || out["probe_degraded"] == json!(true)
588
+ || out["verification_degraded"] == json!(true)
589
+ }
590
+
591
+ fn shutdown_invocation_diagnostic(
592
+ ws: &Path,
593
+ before_len: usize,
594
+ iteration: usize,
595
+ socket: &Path,
596
+ result: &Result<serde_json::Value, crate::cli::CliError>,
597
+ ) -> String {
598
+ let result_summary = match result {
599
+ Ok(out) => format!("report={out}"),
600
+ Err(error) => format!("error={error}"),
601
+ };
602
+ let events = crate::event_log::EventLog::new(ws)
603
+ .tail(0)
604
+ .unwrap_or_else(|error| {
605
+ panic!(
606
+ "shutdown diagnostic event read failed: iteration={iteration} socket={} \
607
+ {result_summary}; error={error}",
608
+ socket.display()
609
+ )
610
+ });
611
+ let appended = events.get(before_len..).unwrap_or_else(|| {
612
+ panic!(
613
+ "shutdown diagnostic boundary invalid: iteration={iteration} socket={} \
614
+ {result_summary}; before_len={before_len} event_len={}",
615
+ socket.display(),
616
+ events.len()
617
+ )
618
+ });
619
+ fn event_name(event: &serde_json::Value) -> Option<&str> {
620
+ event["event"].as_str()
621
+ }
622
+ let starts = appended
623
+ .iter()
624
+ .enumerate()
625
+ .filter(|(_, event)| event_name(event) == Some("lifecycle.shutdown.started"))
626
+ .map(|(index, _)| index)
627
+ .collect::<Vec<_>>();
628
+ let terminals = appended
629
+ .iter()
630
+ .enumerate()
631
+ .filter(|(_, event)| event_name(event) == Some("lifecycle.shutdown"))
632
+ .map(|(index, _)| index)
633
+ .collect::<Vec<_>>();
634
+ if starts.len() != 1 || terminals.len() != 1 || starts[0] >= terminals[0] {
635
+ panic!(
636
+ "shutdown diagnostic markers invalid: iteration={iteration} socket={} \
637
+ {result_summary}; appended={appended:?}",
638
+ socket.display()
639
+ );
640
+ }
641
+ let terminal = &appended[terminals[0]];
642
+ let expected_socket = socket.to_string_lossy();
643
+ if terminal["owned_endpoint"].as_str() != Some(expected_socket.as_ref()) {
644
+ panic!(
645
+ "shutdown diagnostic endpoint mismatch: iteration={iteration} socket={} \
646
+ {result_summary}; terminal={terminal}",
647
+ socket.display()
648
+ );
649
+ }
650
+ let bounded = &appended[starts[0]..=terminals[0]];
651
+ let probe_failures = bounded
652
+ .iter()
653
+ .filter(|event| event_name(event) == Some("shutdown.process_probe_failed"))
654
+ .cloned()
655
+ .collect::<Vec<_>>();
656
+ if probe_failures.is_empty() {
657
+ panic!(
658
+ "shutdown diagnostic missing process-probe failure: iteration={iteration} \
659
+ socket={} {result_summary}; bounded={bounded:?}",
660
+ socket.display()
661
+ );
662
+ }
663
+ for event in &probe_failures {
664
+ let phase = event["phase"].as_str();
665
+ if event["probe"].as_str() != Some("ps_table")
666
+ || !matches!(phase, Some("entry" | "residual_round" | "post_verify"))
667
+ || !matches!(
668
+ event["error"].as_str(),
669
+ Some(error)
670
+ if !error.trim().is_empty()
671
+ && !error.contains("exited with status")
672
+ && (error.contains("No such file") || error.contains("os error 2"))
673
+ )
674
+ {
675
+ panic!(
676
+ "shutdown diagnostic probe failure invalid: iteration={iteration} \
677
+ socket={} {result_summary}; event={event}",
678
+ socket.display()
679
+ );
680
+ }
681
+ }
682
+ format!(
683
+ "iteration={iteration} socket={} {result_summary}; probe_failures={probe_failures:?}",
684
+ socket.display()
685
+ )
686
+ }
687
+
688
+ #[cfg(unix)]
689
+ #[test]
690
+ fn rb07_probe_failure_diagnostic_is_iteration_bound() {
691
+ const CHILD: &str = "RB07_PROBE_FAILURE_DIAGNOSTIC_CHILD";
692
+ const TEST: &str =
693
+ "cli::tests::shutdown_kill_plan::rb07_probe_failure_diagnostic_is_iteration_bound";
694
+ if std::env::var_os(CHILD).is_none() {
695
+ let path = tmp_shutdown_workspace("rb07-empty-path").join("bin");
696
+ std::fs::create_dir_all(&path).unwrap();
697
+ let output = std::process::Command::new(std::env::current_exe().unwrap())
698
+ .args(["--exact", TEST, "--nocapture"])
699
+ .env(CHILD, "1")
700
+ .env("PATH", path)
701
+ .output()
702
+ .unwrap();
703
+ assert!(
704
+ output.status.success(),
705
+ "directed child failed: status={} stdout={} stderr={}",
706
+ output.status,
707
+ String::from_utf8_lossy(&output.stdout),
708
+ String::from_utf8_lossy(&output.stderr)
709
+ );
710
+ let stdout = String::from_utf8_lossy(&output.stdout);
711
+ assert!(
712
+ stdout.contains("running 1 test")
713
+ && stdout.contains(&format!("test {TEST} ..."))
714
+ && stdout.contains("RB07_CHILD_SUCCESS"),
715
+ "directed child omitted success marker: stdout={} stderr={}",
716
+ stdout,
717
+ String::from_utf8_lossy(&output.stderr)
718
+ );
719
+ return;
720
+ }
721
+
722
+ let ws = tmp_shutdown_workspace("rb07-child");
723
+ let socket = ws.join("owned.sock");
724
+ crate::state::persist::save_runtime_state(
725
+ &ws,
726
+ &json!({
727
+ "session_name": "team-rb07-child",
728
+ "tmux_endpoint": socket.to_string_lossy(),
729
+ "tmux_socket": socket.to_string_lossy(),
730
+ "tmux_socket_source": "workspace",
731
+ "is_external_leader": false,
732
+ "agents": {}
733
+ }),
734
+ )
735
+ .unwrap();
736
+ let event_log = crate::event_log::EventLog::new(&ws);
737
+ event_log
738
+ .write(
739
+ "shutdown.process_probe_failed",
740
+ json!({
741
+ "phase": "entry",
742
+ "probe": "ps_table",
743
+ "error": "rb07-stale-decoy"
744
+ }),
745
+ )
746
+ .unwrap();
747
+ let before_len = event_log.tail(0).unwrap().len();
748
+ let result = crate::cli::lifecycle_port::shutdown_with_transport(
749
+ &ws,
750
+ true,
751
+ None,
752
+ &CleanShutdownTransport::new(),
753
+ );
754
+ let diagnostic = shutdown_invocation_diagnostic(&ws, before_len, 0, &socket, &result);
755
+ let failures = match result {
756
+ Ok(out) => {
757
+ assert!(
758
+ shutdown_result_is_degraded(&out),
759
+ "child must force ps failure: {out}"
760
+ );
761
+ let text = diagnostic;
762
+ assert!(
763
+ !text.contains("rb07-stale-decoy"),
764
+ "stale event leaked: {text}"
765
+ );
766
+ text
767
+ }
768
+ Err(error) => panic!("child shutdown unexpectedly returned Err: {error}; {diagnostic}"),
769
+ };
770
+ assert!(
771
+ failures.contains("probe_failures=["),
772
+ "missing bounded failures: {failures}"
773
+ );
774
+ println!("RB07_CHILD_SUCCESS {failures}");
504
775
  }
505
776
 
506
777
  fn tmp_shutdown_workspace(tag: &str) -> PathBuf {
@@ -521,6 +792,7 @@ struct CleanShutdownTransport {
521
792
  kill_server_called: Mutex<bool>,
522
793
  probe_timeout_kind: Option<&'static str>,
523
794
  targets_persist_after_kill: bool,
795
+ kill_session_error: Option<String>,
524
796
  // E49 (0.3.24 P0, shutdown kills leader CLI): record per-pane / per-window /
525
797
  // per-session kill targets so RED contracts can assert (a) the leader pane is
526
798
  // never killed and (b) worker panes ARE killed via the new per-pane path.
@@ -537,6 +809,7 @@ impl CleanShutdownTransport {
537
809
  kill_server_called: Mutex::new(false),
538
810
  probe_timeout_kind: None,
539
811
  targets_persist_after_kill: false,
812
+ kill_session_error: None,
540
813
  killed_panes: Mutex::new(Vec::new()),
541
814
  killed_window_targets: Mutex::new(Vec::new()),
542
815
  killed_sessions: Mutex::new(Vec::new()),
@@ -562,6 +835,11 @@ impl CleanShutdownTransport {
562
835
  self
563
836
  }
564
837
 
838
+ fn with_kill_session_error(mut self, detail: impl Into<String>) -> Self {
839
+ self.kill_session_error = Some(detail.into());
840
+ self
841
+ }
842
+
565
843
  /// E49 (0.3.24 P0): observed per-pane kill targets, in call order.
566
844
  fn killed_panes(&self) -> Vec<String> {
567
845
  self.killed_panes.lock().unwrap().clone()
@@ -675,7 +953,19 @@ impl Transport for CleanShutdownTransport {
675
953
  .unwrap()
676
954
  .push(session.as_str().to_string());
677
955
  *self.session_present.lock().unwrap() = false;
678
- Ok(())
956
+ match self.kill_session_error.as_deref() {
957
+ Some(detail) => Err(TransportError::Subprocess {
958
+ argv: vec![
959
+ "tmux".to_string(),
960
+ "kill-session".to_string(),
961
+ "-t".to_string(),
962
+ session.as_str().to_string(),
963
+ ],
964
+ code: Some(1),
965
+ stderr: detail.to_string(),
966
+ }),
967
+ None => Ok(()),
968
+ }
679
969
  }
680
970
 
681
971
  fn kill_server(&self) -> Result<(), TransportError> {
@@ -1132,6 +1422,8 @@ fn e49_external_leader_shutdown_still_kills_team_session_unconditionally() {
1132
1422
  ])
1133
1423
  .with_targets_persist_after_kill();
1134
1424
 
1425
+ let event_log = crate::event_log::EventLog::new(&ws);
1426
+ let event_start = event_log.tail(0).expect("events before shutdown").len();
1135
1427
  let out = crate::cli::lifecycle_port::shutdown_with_transport(&ws, true, None, &transport)
1136
1428
  .expect("shutdown should complete");
1137
1429
 
@@ -1142,9 +1434,98 @@ fn e49_external_leader_shutdown_still_kills_team_session_unconditionally() {
1142
1434
  the team session — leader pane lives elsewhere so this is safe. Got \
1143
1435
  transport killed_sessions={killed_sessions:?}"
1144
1436
  );
1437
+ if out["ok"] != json!(true) {
1438
+ let events = event_log.tail(0).expect("events after shutdown");
1439
+ let invocation_events = events
1440
+ .into_iter()
1441
+ .skip(event_start)
1442
+ .take(64)
1443
+ .collect::<Vec<_>>();
1444
+ panic!(
1445
+ "E49 external-leader shutdown returned a non-clean outcome: workspace tag=e49-external-leader-still-kills, expected session=team-external, shutdown report={out}, killed_sessions={killed_sessions:?}, bounded invocation events={invocation_events:?}"
1446
+ );
1447
+ }
1145
1448
  assert_eq!(out["ok"], json!(true));
1146
1449
  }
1147
1450
 
1451
+ #[test]
1452
+ fn e49_external_leader_non_clean_diagnostic_is_bounded_and_still_kills() {
1453
+ let ws = tmp_shutdown_workspace("e49-external-leader-non-clean");
1454
+ crate::state::persist::save_runtime_state(
1455
+ &ws,
1456
+ &json!({
1457
+ "session_name": "team-external",
1458
+ "is_external_leader": true,
1459
+ "leader_receiver": {"pane_id": "%leaderelsewhere"},
1460
+ "agents": {
1461
+ "w1": {"status": "running", "provider": "codex", "window": "w1", "pane_id": "%w1"}
1462
+ }
1463
+ }),
1464
+ )
1465
+ .unwrap();
1466
+ let transport = CleanShutdownTransport::new()
1467
+ .with_targets(vec![PaneInfo {
1468
+ pane_id: PaneId::new("%w1"),
1469
+ session: SessionName::new("team-external"),
1470
+ window_index: Some(0),
1471
+ window_name: Some(WindowName::new("w1")),
1472
+ pane_index: Some(0),
1473
+ tty: None,
1474
+ current_command: Some("codex".to_string()),
1475
+ current_path: None,
1476
+ active: true,
1477
+ pane_pid: None,
1478
+ leader_env: BTreeMap::new(),
1479
+ }])
1480
+ .with_kill_session_error("deterministic E49 kill-session failure");
1481
+
1482
+ let event_log = crate::event_log::EventLog::new(&ws);
1483
+ let event_start = event_log.tail(0).expect("events before shutdown").len();
1484
+ let diagnostic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1485
+ let out = crate::cli::lifecycle_port::shutdown_with_transport(&ws, true, None, &transport)
1486
+ .expect("shutdown should complete");
1487
+ let killed_sessions = transport.killed_sessions_observed();
1488
+ assert!(
1489
+ killed_sessions.iter().any(|s| s == "team-external"),
1490
+ concat!(
1491
+ "E49 non-clean path must still attempt kill_session(team-external); ",
1492
+ "got killed_sessions={:?}"
1493
+ ),
1494
+ killed_sessions
1495
+ );
1496
+ if out["ok"] != json!(true) {
1497
+ let events = event_log.tail(0).expect("events after shutdown");
1498
+ let invocation_events = events
1499
+ .into_iter()
1500
+ .skip(event_start)
1501
+ .take(64)
1502
+ .collect::<Vec<_>>();
1503
+ panic!(
1504
+ "E49 external-leader shutdown returned a non-clean outcome: workspace tag=e49-external-leader-non-clean, expected session=team-external, shutdown report={out}, killed_sessions={killed_sessions:?}, bounded invocation events={invocation_events:?}"
1505
+ );
1506
+ }
1507
+ }))
1508
+ .expect_err("deterministic kill-session error must exercise the diagnostic path");
1509
+ let diagnostic = diagnostic
1510
+ .downcast_ref::<String>()
1511
+ .cloned()
1512
+ .or_else(|| diagnostic.downcast_ref::<&str>().map(ToString::to_string))
1513
+ .expect("diagnostic panic should carry text");
1514
+ let bounded_events = diagnostic
1515
+ .split("bounded invocation events=")
1516
+ .nth(1)
1517
+ .expect("diagnostic should include bounded invocation events");
1518
+ let event_count = bounded_events.matches("\"event\":").count();
1519
+ assert!(
1520
+ event_count > 0,
1521
+ "diagnostic should include invocation events"
1522
+ );
1523
+ assert!(
1524
+ event_count <= 64,
1525
+ "diagnostic must be bounded to 64 invocation events, got {event_count}"
1526
+ );
1527
+ }
1528
+
1148
1529
  /// E49 regression guard: managed topology where the leader anchor pane is NOT
1149
1530
  /// in `state.session_name` (e.g. a stale state from an aborted launch where
1150
1531
  /// only worker panes exist in the session). The pre-fix behaviour (kill_session)
@@ -227,13 +227,12 @@ fn rm039_stat001_status_resolves_active_team_when_root_team_key_missing() {
227
227
  #[test]
228
228
  fn status_port_status_compact_json_shape_against_seeded_fixture() {
229
229
  // cmd_status json branch (detail=false) delegates status_port::status(compact=true).
230
- // 0.4.x compact slim: exactly 7 top-level fields; diagnostics moved
231
- // to --detail. Plan: .team/artifacts/status-compact-plan.md.
230
+ // Compact slim is the original 7 keys plus `grok_slot` and
231
+ // `grok_identity_slot_ok`. `ok` is command success (always true here).
232
232
  let ws = seed_status_workspace();
233
233
  let v = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false)
234
234
  .expect("seeded fixture status should project a value");
235
235
  let obj = v.as_object().expect("--json status is a dict");
236
- // Exactly these 7 keys, no more.
237
236
  let expected: std::collections::BTreeSet<&str> = [
238
237
  "ok",
239
238
  "team",
@@ -242,6 +241,8 @@ fn status_port_status_compact_json_shape_against_seeded_fixture() {
242
241
  "ready",
243
242
  "not_ready",
244
243
  "agents",
244
+ "grok_slot",
245
+ "grok_identity_slot_ok",
245
246
  ]
246
247
  .iter()
247
248
  .copied()
@@ -249,7 +250,30 @@ fn status_port_status_compact_json_shape_against_seeded_fixture() {
249
250
  let actual: std::collections::BTreeSet<&str> = obj.keys().map(String::as_str).collect();
250
251
  assert_eq!(
251
252
  actual, expected,
252
- "0.4.x compact must expose exactly 7 keys; got {actual:?}"
253
+ "compact must expose the original 7 keys plus grok_slot and grok_identity_slot_ok (9); got {actual:?}"
254
+ );
255
+ let grok_slot = obj
256
+ .get("grok_slot")
257
+ .and_then(serde_json::Value::as_object)
258
+ .expect("compact must carry grok_slot");
259
+ let grok_ok = grok_slot
260
+ .get("consistent")
261
+ .and_then(serde_json::Value::as_bool)
262
+ == Some(true)
263
+ && grok_slot
264
+ .get("readable")
265
+ .and_then(serde_json::Value::as_bool)
266
+ == Some(true);
267
+ assert_eq!(
268
+ obj.get("ok").and_then(serde_json::Value::as_bool),
269
+ Some(true),
270
+ "compact ok is command success and must stay true; grok_slot={grok_slot:?}"
271
+ );
272
+ assert_eq!(
273
+ obj.get("grok_identity_slot_ok")
274
+ .and_then(serde_json::Value::as_bool),
275
+ Some(grok_ok),
276
+ "grok_identity_slot_ok must follow readable∧consistent; grok_slot={grok_slot:?}"
253
277
  );
254
278
  // Diagnostic keys must NOT leak into the default compact payload.
255
279
  for forbidden in [
@@ -282,6 +306,44 @@ fn status_port_status_compact_json_shape_against_seeded_fixture() {
282
306
  let _ = std::fs::remove_dir_all(&ws);
283
307
  }
284
308
 
309
+ #[test]
310
+ fn status_compact_reports_identity_slot_health_on_grok_identity_slot_ok() {
311
+ // 已废除的行为:旧实现看不见 grok 身份槽。此墓碑留下槽健康断言。
312
+ // 2026-08-18 裁定:`ok` 是命令成功标志,成功投影必须恒真;身份槽健康
313
+ // 走顶层 `grok_identity_slot_ok`,不再重载 `ok`。
314
+ // 2026-08-18 身份串台:judge-g 的产出从 developer-d61 的通道报出并占掉
315
+ // 后者 exactly-once report_result。那时若完全不报槽健康才是一句谎。
316
+ let ws = seed_status_workspace();
317
+ let grok = ws.join(".grok");
318
+ std::fs::create_dir_all(&grok).unwrap();
319
+ std::fs::write(
320
+ grok.join("config.toml"),
321
+ "[mcp_servers.team_orchestrator.env]\nTEAM_AGENT_ID = \"impostor-seat\"\n",
322
+ )
323
+ .unwrap();
324
+
325
+ let v = status_port::status(&ws, /*compact=*/ true, /*detail=*/ false)
326
+ .expect("mismatched grok slot must still project compact status");
327
+ assert_eq!(
328
+ v.get("ok").and_then(serde_json::Value::as_bool),
329
+ Some(true),
330
+ "successful status must keep ok=true; slot health is a sibling key; got {v}"
331
+ );
332
+ assert_eq!(
333
+ v.get("grok_identity_slot_ok")
334
+ .and_then(serde_json::Value::as_bool),
335
+ Some(false),
336
+ "identity-slot mismatch must force grok_identity_slot_ok=false; got {v}"
337
+ );
338
+ assert_eq!(
339
+ v.pointer("/grok_slot/consistent")
340
+ .and_then(serde_json::Value::as_bool),
341
+ Some(false),
342
+ "fixture must actually be a slot mismatch; got {v}"
343
+ );
344
+ let _ = std::fs::remove_dir_all(&ws);
345
+ }
346
+
285
347
  #[test]
286
348
  fn cmd_status_human_appends_harness_reminder() {
287
349
  let ws = seed_status_workspace();
@@ -617,8 +679,14 @@ fn cmd_send_default_human_output_is_one_line_without_false_delivered() {
617
679
  "channel:",
618
680
  "reminder:",
619
681
  ] {
682
+ // 字段级匹配,⛔ 不用裸 contains:人类输出以 " " 连接 `"{key}: {value}"`
683
+ // (send/presentation.rs 的 `parts.join(" ")` / `send_human_field`),
684
+ // 所以一个字段只可能出现在行首或某个空格之后。裸子串会让
685
+ // `turn_verification:` 误命中名单里的 `verification:`(该键由 ab2134c0
686
+ // 有意加进人类输出名单)——那是判据误伤,不是产品缺陷。
687
+ let present = text.starts_with(hidden) || text.contains(&format!(" {hidden}"));
620
688
  assert!(
621
- !text.contains(hidden),
689
+ !present,
622
690
  "default send output should hide {hidden} unless needed; got {text}"
623
691
  );
624
692
  }
@@ -913,10 +981,8 @@ fn run_send_legacy_task_flag_is_internalized_before_persistence() {
913
981
  ExitCode::Ok,
914
982
  "legacy --task is sunset-noticed and ignored; public send persists without caller binding"
915
983
  );
916
- let connection = rusqlite::Connection::open(
917
- ws.join(".team").join("runtime").join("team.db"),
918
- )
919
- .unwrap();
984
+ let connection =
985
+ rusqlite::Connection::open(ws.join(".team").join("runtime").join("team.db")).unwrap();
920
986
  let task_id: Option<String> = connection
921
987
  .query_row(
922
988
  "SELECT task_id FROM messages WHERE content = 'go' ORDER BY rowid DESC LIMIT 1",
@@ -924,7 +990,10 @@ fn run_send_legacy_task_flag_is_internalized_before_persistence() {
924
990
  |row| row.get(0),
925
991
  )
926
992
  .unwrap();
927
- assert_eq!(task_id, None, "caller-supplied task ids must not reach storage");
993
+ assert_eq!(
994
+ task_id, None,
995
+ "caller-supplied task ids must not reach storage"
996
+ );
928
997
  let _ = std::fs::remove_dir_all(&ws);
929
998
  }
930
999
 
@@ -10,7 +10,7 @@ pub enum CommunicationMode {
10
10
 
11
11
  const LEADER_CENTRIC_RUNTIME_CONTRACT: &str = r#"# Team Agent communication contract: leader_centric
12
12
 
13
- - Progress, blockers, questions: team_orchestrator.send_message(to='leader', content='...')
13
+ - Progress, blockers, questions: {send_message}(to='leader', content='...')
14
14
 
15
15
  When you receive a message from the leader or a teammate, you MUST respond
16
16
  through MCP tools. Writing a reply in your terminal does nothing — the sender
@@ -39,10 +39,12 @@ impl CommunicationMode {
39
39
  .find(|mode| mode.as_str() == value)
40
40
  }
41
41
 
42
- pub const fn runtime_contract(self) -> &'static str {
42
+ pub fn runtime_contract(self, send_message: &str) -> String {
43
43
  match self {
44
- Self::LeaderCentric => LEADER_CENTRIC_RUNTIME_CONTRACT,
45
- Self::Orchestrated => ORCHESTRATED_RUNTIME_CONTRACT,
44
+ Self::LeaderCentric => {
45
+ LEADER_CENTRIC_RUNTIME_CONTRACT.replace("{send_message}", send_message)
46
+ }
47
+ Self::Orchestrated => ORCHESTRATED_RUNTIME_CONTRACT.to_string(),
46
48
  }
47
49
  }
48
50
  }