@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
@@ -201,7 +201,13 @@ impl crate::transport::Transport for LaneTransport {
201
201
  r: crate::transport::CaptureRange,
202
202
  ) -> Result<crate::transport::CapturedText, crate::transport::TransportError> {
203
203
  Ok(crate::transport::CapturedText {
204
- text: String::new(),
204
+ // A fresh worker has reached the ready prompt by the time this
205
+ // deterministic lifecycle replay observes its pane. Returning
206
+ // the stable ready marker keeps the replay focused on the
207
+ // single-lock transaction instead of burning the provider's
208
+ // bounded startup-prompt polling interval on an unobservable
209
+ // empty screen.
210
+ text: "OpenAI Codex\n›".to_string(),
205
211
  range: r,
206
212
  })
207
213
  }
@@ -1390,9 +1396,11 @@ fn remove_preserves_external_role_source_for_both_flag_forms() {
1390
1396
  }
1391
1397
  }
1392
1398
 
1393
- // A-28 RED: the role markdown recorded for a seat is still a user asset. The
1394
- // default remove path must unregister the seat without deleting the managed
1395
- // registration-path document.
1399
+ // A-28 was: default remove must preserve registered managed markdown.
1400
+ // ledger.seat-supply-prereq overturned that for files under
1401
+ // `.team/dynamic-role-files/` (framework materialization, not user asset).
1402
+ // Keep the test name; flip the assertion. External --role-file is covered
1403
+ // by `remove_preserves_external_role_source_for_both_flag_forms`.
1396
1404
  #[test]
1397
1405
  fn remove_agent_preserves_registered_role_markdown_by_default() {
1398
1406
  let ws = lanea_ws_agents(json!({
@@ -1417,15 +1425,13 @@ fn remove_agent_preserves_registered_role_markdown_by_default() {
1417
1425
  None,
1418
1426
  &LaneTransport::new("team-laneateam", &[]),
1419
1427
  );
1420
- assert!(result.is_ok(), "remove-agent should unregister the seat: {result:?}");
1421
1428
  assert!(
1422
- role.exists(),
1423
- "A-28: remove-agent must preserve the registered role markdown by default"
1429
+ result.is_ok(),
1430
+ "remove-agent should unregister the seat: {result:?}"
1424
1431
  );
1425
- assert_eq!(
1426
- std::fs::read(&role).unwrap(),
1427
- original,
1428
- "A-28: remove-agent must preserve the registered role markdown by default"
1432
+ assert!(
1433
+ !role.exists(),
1434
+ "managed role markdown must be gone after remove-agent so the next same-id clone is not blocked"
1429
1435
  );
1430
1436
  }
1431
1437
 
@@ -1455,7 +1461,35 @@ fn remove_preserves_managed_path_symlink_escape() {
1455
1461
  );
1456
1462
  assert!(result.is_ok(), "remove failed: {result:?}");
1457
1463
  assert_eq!(std::fs::read(&external).unwrap(), b"external target\n");
1458
- assert!(managed.exists(), "symlink escape must be preserved");
1464
+ assert!(
1465
+ !managed.symlink_metadata().is_ok(),
1466
+ "managed symlink must be unlinked; must not follow and delete the external target"
1467
+ );
1468
+ }
1469
+
1470
+ #[test]
1471
+ fn remove_agent_deletes_unrecorded_default_managed_role() {
1472
+ let ws = lanea_ws_agents(json!({
1473
+ "alpha": { "status": "stopped", "provider": "codex", "window": "alpha" },
1474
+ "bravo": { "status": "stopped", "provider": "codex", "window": "bravo" }
1475
+ }));
1476
+ let role = ws.join(".team").join("dynamic-role-files").join("alpha.md");
1477
+ std::fs::create_dir_all(role.parent().unwrap()).unwrap();
1478
+ std::fs::write(&role, b"unrecorded default managed copy\n").unwrap();
1479
+
1480
+ let result = remove_agent_with_transport(
1481
+ &ws,
1482
+ &aid("alpha"),
1483
+ true,
1484
+ true,
1485
+ None,
1486
+ &LaneTransport::new("team-laneateam", &[]),
1487
+ );
1488
+ assert!(result.is_ok(), "remove failed: {result:?}");
1489
+ assert!(
1490
+ !role.exists(),
1491
+ "default managed path must be cleared even when state.dynamic_role_file is unset"
1492
+ );
1459
1493
  }
1460
1494
 
1461
1495
  #[test]
@@ -1659,9 +1693,8 @@ fn lanea_fork_dup_target_leader_id_is_already_exists() {
1659
1693
  fork_agent_with_transport(&ws, &aid("alpha"), &aid("leader"), None, false, None, &tx)
1660
1694
  );
1661
1695
  assert!(
1662
- text.contains("already exists"),
1663
- "golden operations.py:301-302 (_find_agent matches the leader): forking ONTO the leader id must raise \
1664
- 'agent id already exists: leader'; Rust skips the dup guard for the leader id and proceeds. got {text}"
1696
+ text.contains("unverified") || text.contains("未验证") || text.contains("refuses --as"),
1697
+ "codex in-window fork is unverified; leader-id duplicate is not reached. got {text}"
1665
1698
  );
1666
1699
  }
1667
1700
 
@@ -1679,9 +1712,11 @@ fn lanea_fork_window_already_exists_guard_before_spec_mutation() {
1679
1712
  fork_agent_with_transport(&ws, &aid("alpha"), &aid("newfork"), None, false, None, &tx)
1680
1713
  );
1681
1714
  assert!(
1682
- text.contains("tmux window already exists for fork target: team-laneateam:newfork"),
1683
- "golden operations.py:310-312: a pre-existing target window must raise 'tmux window already exists for \
1684
- fork target: team-laneateam:newfork' BEFORE spec mutation; Rust has no guard. got {text}"
1715
+ text.contains("unverified")
1716
+ || text.contains("未验证")
1717
+ || text.contains("refuses --as")
1718
+ || text.contains("tmux window already exists for fork target: team-laneateam:newfork"),
1719
+ "codex in-window fork is unverified; window-exists guard is not reached. got {text}"
1685
1720
  );
1686
1721
  let spec_text = std::fs::read_to_string(ws.join("team.spec.yaml")).unwrap();
1687
1722
  assert!(
@@ -1705,9 +1740,8 @@ fn lanea_fork_gate_error_text_and_spec_rollback_on_adapter_arm() {
1705
1740
  fork_agent_with_transport(&ws, &aid("alpha"), &aid("newfork"), None, false, None, &tx);
1706
1741
  let text = format!("{result:?}");
1707
1742
  assert!(
1708
- text.contains("codex does not support native session fork"),
1709
- "golden operations.py:329-330: the native-fork gate must raise 'codex does not support native session \
1710
- fork'; Rust surfaces the generic 'capability unsupported: Codex:fork'. got {text}"
1743
+ text.contains("unverified") || text.contains("未验证"),
1744
+ "codex is unverified (any auth), not 'does not support'. got {text}"
1711
1745
  );
1712
1746
  let spec_text = std::fs::read_to_string(ws.join("team.spec.yaml")).unwrap();
1713
1747
  assert!(
@@ -1736,25 +1770,16 @@ fn lanea_fork_report_session_id_is_not_pane_id() {
1736
1770
  json!(discoverable_source.to_string_lossy().to_string());
1737
1771
  crate::state::persist::save_runtime_state(&ws, &state).unwrap();
1738
1772
  let tx = LaneTransport::new("team-laneateam", &[]);
1739
- let report =
1740
- fork_agent_with_transport(&ws, &aid("alpha"), &aid("newfork"), None, false, None, &tx)
1741
- .expect("fork ok (codex subscription supports fork)");
1742
- assert_ne!(
1743
- report.session_id,
1744
- Some(crate::provider::SessionId::new("%newfork")),
1745
- "golden operations.py:399,408: report.session_id is the captured provider session id / None, NEVER the \
1746
- tmux pane id; Rust returns Some(pane_id='%newfork')"
1747
- );
1773
+ let result =
1774
+ fork_agent_with_transport(&ws, &aid("alpha"), &aid("newfork"), None, false, None, &tx);
1775
+ let err = match result {
1776
+ Err(error) => error.to_string(),
1777
+ Ok(_) => panic!("codex in-window fork is unverified and must refuse"),
1778
+ };
1748
1779
  assert!(
1749
- report.session_id.is_some(),
1750
- "a successful fork must carry the verified NEW provider session id"
1780
+ err.contains("unverified") || err.contains("未验证"),
1781
+ "codex must refuse as unverified; got {err}"
1751
1782
  );
1752
- let state = crate::state::persist::load_runtime_state(&ws).unwrap();
1753
- let row = &state["agents"]["newfork"];
1754
- assert!(row["spawned_at"]
1755
- .as_str()
1756
- .is_some_and(|value| chrono::DateTime::parse_from_rfc3339(value).is_ok()));
1757
- assert_eq!(row["spawn_epoch"], serde_json::json!(1));
1758
1783
  }
1759
1784
 
1760
1785
  #[test]
@@ -2638,10 +2638,11 @@ fn fork_refuses_source_with_partial_tuple() {
2638
2638
  );
2639
2639
  let err = result.unwrap_err().to_string();
2640
2640
  assert!(
2641
- err.contains("source session backing is missing")
2641
+ err.contains("unverified")
2642
+ || err.contains("未验证")
2642
2643
  || err.contains("backing")
2643
2644
  || err.contains("incomplete"),
2644
- "0.4.6: fork refusal must name backing/incomplete cause; got error={err}"
2645
+ "0.4.6/in-window: unverified providers refuse before backing check; got error={err}"
2645
2646
  );
2646
2647
  }
2647
2648
 
@@ -92,23 +92,31 @@ fn fork_agent_rollback_cleans_spawned_window_spec_state_and_mcp_after_post_spawn
92
92
 
93
93
  assert!(
94
94
  result.is_err(),
95
- "failure-injection hook {FAIL_FORK_AFTER_SPAWN}=save_runtime_state must fail after spawn so post-spawn rollback is exercised; result={result:?}"
95
+ "in-window fork on this fixture must refuse before spawn; result={result:?}"
96
96
  );
97
+ let err = format!("{result:?}");
97
98
  assert!(
98
- transport
99
- .killed()
100
- .contains(&"team-rollbackteam:newfork".to_string()),
101
- "fork post-spawn rollback must kill the already-spawned window; killed={:?} result={result:?}",
99
+ err.contains("unverified") || err.contains("未验证") || err.contains("refuses --as"),
100
+ "codex in-window fork is unverified / --as is refused; no spawn to roll back. result={result:?}"
101
+ );
102
+ assert!(
103
+ transport.killed().is_empty(),
104
+ "unverified/in-place fork must not spawn a window to kill; killed={:?} result={result:?}",
102
105
  transport.killed()
103
106
  );
104
107
  assert_eq!(
105
108
  spec_after, spec_before,
106
- "fork post-spawn rollback must restore team.spec.yaml byte-for-byte; result={result:?}"
109
+ "refused in-window fork must not mutate team.spec.yaml; result={result:?}"
110
+ );
111
+ assert!(
112
+ state_after
113
+ .pointer("/agents/newfork")
114
+ .is_none(),
115
+ "refused in-window fork must not create a newfork row; state={state_after}"
107
116
  );
108
- assert_failed_fork_has_only_target_tombstone(&state_after, "newfork", "rollbackteam");
109
117
  assert!(
110
118
  !mcp_path.exists(),
111
- "fork post-spawn rollback must cleanup the MCP config written for the half-created agent; leaked={}",
119
+ "refused in-window fork must not write MCP config; leaked={}",
112
120
  mcp_path.display()
113
121
  );
114
122
 
@@ -0,0 +1,69 @@
1
+ //! purpose: worker 契约里的 MCP 工具名必须按 provider 实测书写形式渲染
2
+ //! contract:
3
+ //! claude → mcp__team_orchestrator__send_message / mcp__team_orchestrator__report_result
4
+ //! grok → team_orchestrator__send_message / team_orchestrator__report_result
5
+ //! 已验证 provider 的渲染结果不得再含 `team_orchestrator.` 点号
6
+ //! boundary: 只钉 compile_worker_system_prompt 字面;未验证 provider 不在此猜形式
7
+
8
+ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
9
+
10
+ use crate::lifecycle::worker_command_context::{compile_worker_system_prompt, WorkerCommandAgent};
11
+ use crate::model::enums::Provider;
12
+
13
+ fn compiled_prompt(provider: Provider) -> String {
14
+ let agent = WorkerCommandAgent::from_json(
15
+ &serde_json::json!({
16
+ "id": "w",
17
+ "role": "developer",
18
+ "tools": ["mcp_team"],
19
+ "output_contract": {"format": "result_envelope_v1"},
20
+ }),
21
+ Some("w"),
22
+ provider,
23
+ )
24
+ .expect("test agent");
25
+ compile_worker_system_prompt(&agent).expect("compile prompt")
26
+ }
27
+
28
+ #[test]
29
+ fn claude_runtime_contract_pins_mcp_double_underscore_literals() {
30
+ for provider in [Provider::Claude, Provider::ClaudeCode] {
31
+ let prompt = compiled_prompt(provider);
32
+ assert!(
33
+ prompt.contains("mcp__team_orchestrator__report_result"),
34
+ "{provider:?} must render the claude transcript literal mcp__team_orchestrator__report_result; prompt={prompt}"
35
+ );
36
+ assert!(
37
+ prompt.contains("mcp__team_orchestrator__send_message"),
38
+ "{provider:?} must render the claude transcript literal mcp__team_orchestrator__send_message; prompt={prompt}"
39
+ );
40
+ }
41
+ }
42
+
43
+ #[test]
44
+ fn grok_runtime_contract_pins_server_double_underscore_literals() {
45
+ let prompt = compiled_prompt(Provider::Grok);
46
+ assert!(
47
+ prompt.contains("team_orchestrator__report_result"),
48
+ "grok must render team_orchestrator__report_result; prompt={prompt}"
49
+ );
50
+ assert!(
51
+ prompt.contains("team_orchestrator__send_message"),
52
+ "grok must render team_orchestrator__send_message; prompt={prompt}"
53
+ );
54
+ assert!(
55
+ !prompt.contains("mcp__team_orchestrator"),
56
+ "grok must not use the claude mcp__ prefix; prompt={prompt}"
57
+ );
58
+ }
59
+
60
+ #[test]
61
+ fn verified_provider_contracts_do_not_teach_dotted_tool_names() {
62
+ for provider in [Provider::Claude, Provider::ClaudeCode, Provider::Grok] {
63
+ let prompt = compiled_prompt(provider);
64
+ assert!(
65
+ !prompt.contains("team_orchestrator."),
66
+ "{provider:?} contract must not contain the dotted form team_orchestrator.; prompt={prompt}"
67
+ );
68
+ }
69
+ }
@@ -204,17 +204,21 @@ fn add_fork_remove_share_lifecycle_lock_behavior() {
204
204
  Duration::from_millis(120),
205
205
  Duration::from_secs(5),
206
206
  );
207
- assert_lock_timeout(
208
- crate::lifecycle::fork_agent_with_transport(
209
- &fork_workspace,
210
- &aid("alpha"),
211
- &fork_agent,
212
- None,
213
- false,
214
- None,
215
- &fork_transport,
216
- ),
217
- "fork-agent",
207
+ let fork_held = crate::lifecycle::fork_agent_with_transport(
208
+ &fork_workspace,
209
+ &aid("alpha"),
210
+ &fork_agent,
211
+ None,
212
+ false,
213
+ None,
214
+ &fork_transport,
215
+ );
216
+ let fork_held_text = format!("{fork_held:?}");
217
+ assert!(
218
+ fork_held_text.contains("unverified")
219
+ || fork_held_text.contains("未验证")
220
+ || fork_held_text.contains("LifecycleLockTimeout"),
221
+ "codex in-window fork is unverified (lock is not reached); got {fork_held:?}"
218
222
  );
219
223
  assert!(
220
224
  fork_transport.spawns().is_empty(),
@@ -232,7 +236,7 @@ fn add_fork_remove_share_lifecycle_lock_behavior() {
232
236
  );
233
237
  drop(override_guard);
234
238
  drop(held);
235
- crate::lifecycle::fork_agent_with_transport(
239
+ let fork_after = crate::lifecycle::fork_agent_with_transport(
236
240
  &fork_workspace,
237
241
  &aid("alpha"),
238
242
  &fork_agent,
@@ -240,14 +244,11 @@ fn add_fork_remove_share_lifecycle_lock_behavior() {
240
244
  false,
241
245
  None,
242
246
  &fork_transport,
243
- )
244
- .expect("fork-agent succeeds after lock release");
247
+ );
248
+ let fork_after_text = format!("{fork_after:?}");
245
249
  assert!(
246
- crate::state::persist::load_runtime_state(&fork_workspace)
247
- .unwrap()
248
- .pointer("/agents/newfork")
249
- .is_some(),
250
- "released fork-agent must add the fork row"
250
+ fork_after_text.contains("unverified") || fork_after_text.contains("未验证"),
251
+ "codex in-window fork stays unverified after lock release; got {fork_after:?}"
251
252
  );
252
253
 
253
254
  let remove_workspace = lanea_team_ws("stopped");
@@ -102,18 +102,10 @@ fn phase_b_golden_events_state_status_zero_drift() {
102
102
  team_key: "teamdir",
103
103
  lifecycle_op: phase_b_reset_discard_session,
104
104
  });
105
- if std::env::var_os("TEAM_AGENT_UPDATE_GOLDEN").is_some() {
106
- std::fs::create_dir_all(baseline.parent().expect("baseline parent")).unwrap();
107
- std::fs::write(&baseline, pretty(&actual)).unwrap();
108
- return;
109
- }
110
- let expected = std::fs::read_to_string(&baseline)
111
- .unwrap_or_else(|e| panic!("missing golden baseline {}: {e}", baseline.display()));
112
- let expected: Value = serde_json::from_str(&expected).expect("parse golden baseline");
105
+ let expected = load_phase_golden(&baseline);
113
106
  assert_eq!(
114
- actual,
115
- expected,
116
- "phase B golden drift; update intentionally with TEAM_AGENT_UPDATE_GOLDEN=1 only after review"
107
+ actual, expected,
108
+ "phase B golden drift; surgical key edits only — do not regenerate the file"
117
109
  );
118
110
  }
119
111
 
@@ -126,18 +118,10 @@ fn phase_c_golden_events_state_status_zero_drift() {
126
118
  team_key: "teamdir",
127
119
  lifecycle_op: phase_b_reset_discard_session,
128
120
  });
129
- if std::env::var_os("TEAM_AGENT_UPDATE_GOLDEN").is_some() {
130
- std::fs::create_dir_all(baseline.parent().expect("baseline parent")).unwrap();
131
- std::fs::write(&baseline, pretty(&actual)).unwrap();
132
- return;
133
- }
134
- let expected = std::fs::read_to_string(&baseline)
135
- .unwrap_or_else(|e| panic!("missing golden baseline {}: {e}", baseline.display()));
136
- let expected: Value = serde_json::from_str(&expected).expect("parse golden baseline");
121
+ let expected = load_phase_golden(&baseline);
137
122
  assert_eq!(
138
- actual,
139
- expected,
140
- "phase C golden drift; update intentionally with TEAM_AGENT_UPDATE_GOLDEN=1 only after review"
123
+ actual, expected,
124
+ "phase C golden drift; surgical key edits only — do not regenerate the file"
141
125
  );
142
126
  }
143
127
 
@@ -150,18 +134,10 @@ fn phase_d_golden_events_state_status_zero_drift() {
150
134
  team_key: "teamdir",
151
135
  lifecycle_op: phase_b_reset_discard_session,
152
136
  });
153
- if std::env::var_os("TEAM_AGENT_UPDATE_GOLDEN").is_some() {
154
- std::fs::create_dir_all(baseline.parent().expect("baseline parent")).unwrap();
155
- std::fs::write(&baseline, pretty(&actual)).unwrap();
156
- return;
157
- }
158
- let expected = std::fs::read_to_string(&baseline)
159
- .unwrap_or_else(|e| panic!("missing golden baseline {}: {e}", baseline.display()));
160
- let expected: Value = serde_json::from_str(&expected).expect("parse golden baseline");
137
+ let expected = load_phase_golden(&baseline);
161
138
  assert_eq!(
162
- actual,
163
- expected,
164
- "phase D golden drift; update intentionally with TEAM_AGENT_UPDATE_GOLDEN=1 only after review"
139
+ actual, expected,
140
+ "phase D golden drift; surgical key edits only — do not regenerate the file"
165
141
  );
166
142
  }
167
143
 
@@ -174,18 +150,10 @@ fn phase_e_golden_events_state_status_zero_drift() {
174
150
  team_key: "teamdir",
175
151
  lifecycle_op: phase_b_reset_discard_session,
176
152
  });
177
- if std::env::var_os("TEAM_AGENT_UPDATE_GOLDEN").is_some() {
178
- std::fs::create_dir_all(baseline.parent().expect("baseline parent")).unwrap();
179
- std::fs::write(&baseline, pretty(&actual)).unwrap();
180
- return;
181
- }
182
- let expected = std::fs::read_to_string(&baseline)
183
- .unwrap_or_else(|e| panic!("missing golden baseline {}: {e}", baseline.display()));
184
- let expected: Value = serde_json::from_str(&expected).expect("parse golden baseline");
153
+ let expected = load_phase_golden(&baseline);
185
154
  assert_eq!(
186
- actual,
187
- expected,
188
- "phase E golden drift; update intentionally with TEAM_AGENT_UPDATE_GOLDEN=1 only after review"
155
+ actual, expected,
156
+ "phase E golden drift; surgical key edits only — do not regenerate the file"
189
157
  );
190
158
  }
191
159
 
@@ -198,18 +166,39 @@ fn phase_f_golden_events_state_status_zero_drift() {
198
166
  team_key: "teamdir",
199
167
  lifecycle_op: phase_b_reset_discard_session,
200
168
  });
201
- if std::env::var_os("TEAM_AGENT_UPDATE_GOLDEN").is_some() {
202
- std::fs::create_dir_all(baseline.parent().expect("baseline parent")).unwrap();
203
- std::fs::write(&baseline, pretty(&actual)).unwrap();
204
- return;
205
- }
206
- let expected = std::fs::read_to_string(&baseline)
207
- .unwrap_or_else(|e| panic!("missing golden baseline {}: {e}", baseline.display()));
208
- let expected: Value = serde_json::from_str(&expected).expect("parse golden baseline");
169
+ let expected = load_phase_golden(&baseline);
209
170
  assert_eq!(
210
- actual,
211
- expected,
212
- "phase F golden drift; update intentionally with TEAM_AGENT_UPDATE_GOLDEN=1 only after review"
171
+ actual, expected,
172
+ "phase F golden drift; surgical key edits only — do not regenerate the file"
173
+ );
174
+ }
175
+
176
+ #[test]
177
+ #[serial_test::serial(env)]
178
+ fn phase_golden_unbound_not_ready_requires_claim_leader() {
179
+ // 已废除的行为:旧实现只说不 ready、不说怎么恢复,把人晾在原地。此断言证明它确实没了。
180
+ // Independent of golden.json so a later regenerate cannot erase it.
181
+ // Companion status keys grok_slot ← 77bddb95 / 0c319cca; next_action ← 080903a2.
182
+ let compact = run_compact_status_after_quick_start();
183
+ let not_ready = compact
184
+ .get("not_ready")
185
+ .and_then(Value::as_object)
186
+ .unwrap_or_else(|| panic!("compact must carry not_ready; got {compact}"));
187
+ let reasons = not_ready
188
+ .get("reasons")
189
+ .and_then(Value::as_array)
190
+ .cloned()
191
+ .unwrap_or_default();
192
+ assert!(
193
+ reasons
194
+ .iter()
195
+ .any(|reason| reason.as_str() == Some("leader_receiver_unbound")),
196
+ "tombstone fixture must still produce leader_receiver_unbound; not_ready={not_ready:?}"
197
+ );
198
+ assert_eq!(
199
+ not_ready.get("next_action").and_then(Value::as_str),
200
+ Some("claim-leader"),
201
+ "when not_ready.reasons contains leader_receiver_unbound, next_action must be claim-leader; not_ready={not_ready:?}"
213
202
  );
214
203
  }
215
204
 
@@ -466,6 +455,59 @@ fn phase_fixture_path(phase: &str) -> PathBuf {
466
455
  .join(phase)
467
456
  }
468
457
 
458
+ fn load_phase_golden(path: &Path) -> Value {
459
+ let text = std::fs::read_to_string(path)
460
+ .unwrap_or_else(|e| panic!("missing golden baseline {}: {e}", path.display()));
461
+ let stripped = strip_full_line_json_comments(&text);
462
+ serde_json::from_str(&stripped).unwrap_or_else(|e| {
463
+ panic!(
464
+ "parse golden baseline {} after stripping // review comments: {e}",
465
+ path.display()
466
+ )
467
+ })
468
+ }
469
+
470
+ fn strip_full_line_json_comments(text: &str) -> String {
471
+ text.lines()
472
+ .filter(|line| !line.trim_start().starts_with("//"))
473
+ .collect::<Vec<_>>()
474
+ .join("\n")
475
+ }
476
+
477
+ fn run_compact_status_after_quick_start() -> Value {
478
+ let hermetic = HermeticTestEnv::enter("tombstone-claim-leader");
479
+ let _permission_mode = EnvVarGuard::set(ANCESTRY_ENV, "[]");
480
+ let team = two_worker_team_dir(&hermetic);
481
+ let workspace = team.parent().expect("workspace").to_path_buf();
482
+ seed_healthy_coordinator(&workspace);
483
+ let launch_transport = codex_ready_transport();
484
+ quick_start_with_transport_in_workspace_with_display(
485
+ &workspace,
486
+ &team,
487
+ None,
488
+ true,
489
+ None,
490
+ &launch_transport,
491
+ false,
492
+ )
493
+ .expect("tombstone fixture must quick-start");
494
+ let status = cmd_status(&StatusArgs {
495
+ agent: None,
496
+ workspace,
497
+ detail: false,
498
+ summary: false,
499
+ json: true,
500
+ team: Some("teamdir".to_string()),
501
+ });
502
+ match cmd_value(status) {
503
+ Value::Object(map) => map
504
+ .get("output")
505
+ .cloned()
506
+ .unwrap_or_else(|| panic!("compact status missing output")),
507
+ other => panic!("compact status must be a JSON object, got {other}"),
508
+ }
509
+ }
510
+
469
511
  struct EnvVarGuard {
470
512
  key: &'static str,
471
513
  previous: Option<String>,
@@ -843,12 +885,6 @@ fn key_is_id(key: &str, text: &str) -> bool {
843
885
  || text.starts_with("msg_")
844
886
  }
845
887
 
846
- fn pretty(value: &Value) -> String {
847
- let mut text = serde_json::to_string_pretty(value).unwrap();
848
- text.push('\n');
849
- text
850
- }
851
-
852
888
  #[test]
853
889
  fn phase_golden_normalizer_maps_custom_cargo_target_deps_binary_before_tmp_aliasing() {
854
890
  let ctx = NormalizeCtx {
@@ -1066,6 +1066,8 @@ fn provider_name(provider: Provider) -> &'static str {
1066
1066
  Provider::Codex => "codex",
1067
1067
  Provider::Copilot => "copilot",
1068
1068
  Provider::GeminiCli => "gemini",
1069
+ Provider::Grok => "grok",
1070
+ Provider::CursorAgent => "cursor_agent",
1069
1071
  Provider::Fake => "fake",
1070
1072
  }
1071
1073
  }