@team-agent/installer 0.5.42 → 0.5.44

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 (156) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +129 -54
  4. package/crates/team-agent/src/cli/diagnose.rs +1 -2
  5. package/crates/team-agent/src/cli/emit.rs +1 -7
  6. package/crates/team-agent/src/cli/helpers.rs +3 -1
  7. package/crates/team-agent/src/cli/leader.rs +2 -1
  8. package/crates/team-agent/src/cli/mod.rs +126 -16
  9. package/crates/team-agent/src/cli/named_address.rs +14 -5
  10. package/crates/team-agent/src/cli/profile.rs +19 -7
  11. package/crates/team-agent/src/cli/send.rs +9 -3
  12. package/crates/team-agent/src/cli/status.rs +8 -30
  13. package/crates/team-agent/src/cli/status_port.rs +1341 -1272
  14. package/crates/team-agent/src/cli/tests/base.rs +738 -660
  15. package/crates/team-agent/src/cli/tests/compile.rs +45 -18
  16. package/crates/team-agent/src/cli/tests/divergence.rs +462 -444
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +365 -282
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +356 -329
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +672 -564
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +284 -224
  21. package/crates/team-agent/src/cli/tests/mod.rs +17 -7
  22. package/crates/team-agent/src/cli/tests/named_address.rs +8 -2
  23. package/crates/team-agent/src/cli/tests/peer_allow.rs +10 -2
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +314 -273
  25. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +26 -15
  26. package/crates/team-agent/src/cli/tests/status_send.rs +707 -629
  27. package/crates/team-agent/src/cli/tests/verb_install_skill.rs +20 -4
  28. package/crates/team-agent/src/cli/tests/verb_profile.rs +46 -17
  29. package/crates/team-agent/src/cli/tests/verb_validate.rs +15 -3
  30. package/crates/team-agent/src/codex_app_server.rs +2 -5
  31. package/crates/team-agent/src/compiler/tests.rs +139 -33
  32. package/crates/team-agent/src/compiler.rs +55 -22
  33. package/crates/team-agent/src/conpty/backend.rs +23 -33
  34. package/crates/team-agent/src/coordinator/backoff.rs +2 -7
  35. package/crates/team-agent/src/coordinator/conpty_shim.rs +55 -67
  36. package/crates/team-agent/src/coordinator/health.rs +46 -31
  37. package/crates/team-agent/src/coordinator/mod.rs +3 -3
  38. package/crates/team-agent/src/coordinator/orphan.rs +22 -10
  39. package/crates/team-agent/src/coordinator/steps/abnormal.rs +51 -56
  40. package/crates/team-agent/src/coordinator/tests/abnormal.rs +55 -19
  41. package/crates/team-agent/src/coordinator/tests/basics.rs +179 -41
  42. package/crates/team-agent/src/coordinator/tests/daemon.rs +53 -13
  43. package/crates/team-agent/src/coordinator/tests/health_sync.rs +78 -19
  44. package/crates/team-agent/src/coordinator/tests/main_preserved.rs +61 -11
  45. package/crates/team-agent/src/coordinator/tests/mod.rs +33 -39
  46. package/crates/team-agent/src/coordinator/tests/spine.rs +52 -12
  47. package/crates/team-agent/src/coordinator/tests/takeover.rs +73 -15
  48. package/crates/team-agent/src/coordinator/tests/tick_core.rs +50 -15
  49. package/crates/team-agent/src/coordinator/tests/watch.rs +74 -20
  50. package/crates/team-agent/src/db/message_store.rs +138 -30
  51. package/crates/team-agent/src/db/migration.rs +249 -61
  52. package/crates/team-agent/src/db/schema.rs +303 -82
  53. package/crates/team-agent/src/diagnose/comms.rs +9 -2
  54. package/crates/team-agent/src/diagnose/mod.rs +1 -3
  55. package/crates/team-agent/src/diagnose/orphans.rs +79 -61
  56. package/crates/team-agent/src/event_log.rs +70 -16
  57. package/crates/team-agent/src/layout/manager.rs +15 -4
  58. package/crates/team-agent/src/layout/mod.rs +4 -4
  59. package/crates/team-agent/src/layout/overlay.rs +10 -3
  60. package/crates/team-agent/src/layout/placement.rs +5 -1
  61. package/crates/team-agent/src/layout/recovery.rs +4 -2
  62. package/crates/team-agent/src/layout/runtime_sessions.rs +7 -7
  63. package/crates/team-agent/src/layout/sessions.rs +17 -9
  64. package/crates/team-agent/src/layout/tmux_endpoint.rs +1 -1
  65. package/crates/team-agent/src/layout/worker_env.rs +87 -19
  66. package/crates/team-agent/src/leader/helpers.rs +7 -1
  67. package/crates/team-agent/src/leader/lease.rs +199 -89
  68. package/crates/team-agent/src/leader/owner_bind.rs +55 -22
  69. package/crates/team-agent/src/leader/provider_attribution.rs +25 -6
  70. package/crates/team-agent/src/leader/rediscover/tests.rs +88 -24
  71. package/crates/team-agent/src/leader/rediscover.rs +74 -25
  72. package/crates/team-agent/src/leader/registry.rs +1 -1
  73. package/crates/team-agent/src/leader/start.rs +75 -54
  74. package/crates/team-agent/src/leader/takeover.rs +46 -11
  75. package/crates/team-agent/src/leader/tests/basics.rs +320 -167
  76. package/crates/team-agent/src/leader/tests/byte_findings.rs +361 -219
  77. package/crates/team-agent/src/leader/tests/identity.rs +428 -356
  78. package/crates/team-agent/src/leader/tests/idle.rs +285 -254
  79. package/crates/team-agent/src/leader/tests/lease_api.rs +338 -274
  80. package/crates/team-agent/src/leader/tests/lease_claim.rs +643 -593
  81. package/crates/team-agent/src/leader/tests/mod.rs +115 -99
  82. package/crates/team-agent/src/leader/tests/rediscover.rs +74 -22
  83. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +237 -211
  84. package/crates/team-agent/src/lib.rs +4 -4
  85. package/crates/team-agent/src/lifecycle/display.rs +7 -3
  86. package/crates/team-agent/src/lifecycle/launch.rs +55 -15
  87. package/crates/team-agent/src/lifecycle/mod.rs +9 -1
  88. package/crates/team-agent/src/lifecycle/profile_launch.rs +77 -34
  89. package/crates/team-agent/src/lifecycle/profile_smoke.rs +3 -1
  90. package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -6
  91. package/crates/team-agent/src/lifecycle/restart/common.rs +6 -2
  92. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +1 -4
  93. package/crates/team-agent/src/lifecycle/restart/preflight.rs +6 -5
  94. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +26 -22
  95. package/crates/team-agent/src/lifecycle/restart/remove.rs +45 -35
  96. package/crates/team-agent/src/lifecycle/restart/team_state.rs +63 -17
  97. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +251 -84
  98. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +198 -48
  99. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +2 -1
  100. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +152 -32
  101. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +1 -5
  102. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +3 -0
  103. package/crates/team-agent/src/lifecycle/tests.rs +2 -2
  104. package/crates/team-agent/src/main.rs +4 -4
  105. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +47 -20
  106. package/crates/team-agent/src/mcp_server/mod.rs +11 -2
  107. package/crates/team-agent/src/mcp_server/types.rs +14 -3
  108. package/crates/team-agent/src/mcp_server/wire.rs +230 -68
  109. package/crates/team-agent/src/messaging/delivery.rs +20 -18
  110. package/crates/team-agent/src/messaging/helpers.rs +25 -4
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +4 -5
  112. package/crates/team-agent/src/messaging/mod.rs +2 -3
  113. package/crates/team-agent/src/messaging/selftest.rs +46 -26
  114. package/crates/team-agent/src/messaging/tests/main_preserved.rs +47 -12
  115. package/crates/team-agent/src/messaging/tests/runtime.rs +57 -22
  116. package/crates/team-agent/src/messaging/tests/spine.rs +154 -40
  117. package/crates/team-agent/src/messaging/tests/wave2.rs +31 -32
  118. package/crates/team-agent/src/messaging/trust.rs +25 -2
  119. package/crates/team-agent/src/messaging/watchers.rs +37 -9
  120. package/crates/team-agent/src/model/enums.rs +65 -16
  121. package/crates/team-agent/src/model/ids.rs +12 -3
  122. package/crates/team-agent/src/model/paths.rs +28 -7
  123. package/crates/team-agent/src/model/permissions.rs +176 -33
  124. package/crates/team-agent/src/model/routing.rs +66 -20
  125. package/crates/team-agent/src/model/spec.rs +365 -69
  126. package/crates/team-agent/src/model/task_graph.rs +36 -9
  127. package/crates/team-agent/src/model/yaml/tests.rs +24 -6
  128. package/crates/team-agent/src/model/yaml.rs +7 -6
  129. package/crates/team-agent/src/packaging/install.rs +23 -9
  130. package/crates/team-agent/src/packaging/migrate.rs +5 -7
  131. package/crates/team-agent/src/packaging/mod.rs +9 -1
  132. package/crates/team-agent/src/packaging/repair.rs +13 -6
  133. package/crates/team-agent/src/packaging/tests.rs +63 -16
  134. package/crates/team-agent/src/packaging/types.rs +22 -7
  135. package/crates/team-agent/src/platform/argv.rs +4 -1
  136. package/crates/team-agent/src/platform/file_lock.rs +22 -8
  137. package/crates/team-agent/src/platform/process.rs +54 -24
  138. package/crates/team-agent/src/provider/adapters/claude.rs +1 -3
  139. package/crates/team-agent/src/provider/approvals/parsing.rs +134 -26
  140. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +13 -3
  141. package/crates/team-agent/src/provider/classify.rs +127 -42
  142. package/crates/team-agent/src/provider/faults.rs +17 -5
  143. package/crates/team-agent/src/provider/helpers.rs +6 -5
  144. package/crates/team-agent/src/provider/startup_prompt.rs +75 -27
  145. package/crates/team-agent/src/state/persist.rs +28 -0
  146. package/crates/team-agent/src/state/repository.rs +8 -3
  147. package/crates/team-agent/src/tmux_backend/tests.rs +1637 -1398
  148. package/crates/team-agent/src/tmux_backend.rs +80 -44
  149. package/crates/team-agent/src/topology.rs +40 -20
  150. package/crates/team-agent/src/transport/test_support.rs +20 -23
  151. package/crates/team-agent/src/transport/tests/behavior.rs +292 -293
  152. package/crates/team-agent/src/transport/tests/mod.rs +178 -187
  153. package/crates/team-agent/src/transport/tests/wire.rs +561 -525
  154. package/crates/team-agent/src/transport.rs +12 -17
  155. package/crates/team-agent/src/transport_factory.rs +29 -14
  156. package/package.json +4 -4
@@ -1,217 +1,243 @@
1
1
  use super::*;
2
2
 
3
- // =====================================================================
4
- // 4. wake 层纯函数(unimplemented → RED)。golden:probe_leader.py 5 分支 + boundary。
5
- // =====================================================================
6
-
7
- #[test]
8
- fn should_reread_no_file_when_current_mtime_none() {
9
- // current_mtime is None → {reread:false, no_file}(wake.py:31-32)。
10
- let d = should_reread(None, None, None, 100.0, 60.0);
11
- assert!(!d.reread);
12
- assert_eq!(d.reason, RereadReason::NoFile);
3
+ // =====================================================================
4
+ // 4. wake 层纯函数(unimplemented → RED)。golden:probe_leader.py 5 分支 + boundary。
5
+ // =====================================================================
6
+
7
+ #[test]
8
+ fn should_reread_no_file_when_current_mtime_none() {
9
+ // current_mtime is None → {reread:false, no_file}(wake.py:31-32)。
10
+ let d = should_reread(None, None, None, 100.0, 60.0);
11
+ assert!(!d.reread);
12
+ assert_eq!(d.reason, RereadReason::NoFile);
13
+ }
14
+
15
+ #[test]
16
+ fn should_reread_never_classified() {
17
+ // last_classified_mtime is None → {reread:true, never_classified}。
18
+ let d = should_reread(None, Some(10.0), None, 100.0, 60.0);
19
+ assert!(d.reread);
20
+ assert_eq!(d.reason, RereadReason::NeverClassified);
21
+ }
22
+
23
+ #[test]
24
+ fn should_reread_file_changed() {
25
+ // current != last_classified → {reread:true, file_changed}。
26
+ let d = should_reread(Some(5.0), Some(20.0), Some(10.0), 100.0, 60.0);
27
+ assert!(d.reread);
28
+ assert_eq!(d.reason, RereadReason::FileChanged);
29
+ }
30
+
31
+ #[test]
32
+ fn should_reread_quiescent_already_classified_at_and_past_debounce() {
33
+ // current==last_classified 且 silent_for >= debounce → quiescent(不再重读)。
34
+ // silent_for = max(0, now-current_mtime) = 100-10 = 90 >= 60。
35
+ let d = should_reread(Some(10.0), Some(10.0), Some(10.0), 100.0, 60.0);
36
+ assert!(!d.reread);
37
+ assert_eq!(d.reason, RereadReason::QuiescentAlreadyClassified);
38
+ // 边界:silent_for 恰 == debounce(now-current=60)→ 仍 quiescent(>= 比较)。
39
+ let b = should_reread(Some(40.0), Some(40.0), Some(40.0), 100.0, 60.0);
40
+ assert!(!b.reread);
41
+ assert_eq!(b.reason, RereadReason::QuiescentAlreadyClassified);
42
+ }
43
+
44
+ #[test]
45
+ fn should_reread_unchanged_within_debounce() {
46
+ // current==last_classified 且 silent_for < debounce → unchanged。
47
+ // now-current = 100-95 = 5 < 60。注:last_mtime 在 Python body 中未被使用。
48
+ let d = should_reread(Some(10.0), Some(95.0), Some(95.0), 100.0, 60.0);
49
+ assert!(!d.reread);
50
+ assert_eq!(d.reason, RereadReason::Unchanged);
51
+ }
52
+
53
+ #[test]
54
+ fn on_file_changed_adds_node_sorted_and_records_mtime() {
55
+ // golden probe_leader.py:add b → pending ["b"];add a → sorted ["a","b"]。
56
+ let s0 = on_file_changed(None, "b", 1.0);
57
+ assert_eq!(s0.pending, vec!["b".to_string()]);
58
+ assert_eq!(s0.mtimes.get("b"), Some(&1.0));
59
+ let s1 = on_file_changed(Some(&s0), "a", 2.0);
60
+ assert_eq!(
61
+ s1.pending,
62
+ vec!["a".to_string(), "b".to_string()],
63
+ "pending 必须 sorted"
64
+ );
65
+ assert_eq!(s1.mtimes.get("a"), Some(&2.0));
66
+ // 重复 add b:pending 仍是 set(不重复),mtime 被更新到 3.0。
67
+ let s2 = on_file_changed(Some(&s1), "b", 3.0);
68
+ assert_eq!(s2.pending, vec!["a".to_string(), "b".to_string()]);
69
+ assert_eq!(s2.mtimes.get("b"), Some(&3.0));
70
+ }
71
+
72
+ #[test]
73
+ fn take_pending_drains_sorted_and_clears_pending_but_keeps_mtimes() {
74
+ // golden probe_leader.py:drain → (["a","b"], state{pending:[], mtimes 保留})。
75
+ let mut st = on_file_changed(None, "b", 3.0);
76
+ st = on_file_changed(Some(&st), "a", 2.0);
77
+ let (drained, after) = take_pending(Some(&st));
78
+ assert_eq!(drained, vec!["a".to_string(), "b".to_string()]);
79
+ assert!(after.pending.is_empty());
80
+ // mtimes 不被 drain 清空。
81
+ assert_eq!(after.mtimes.get("a"), Some(&2.0));
82
+ assert_eq!(after.mtimes.get("b"), Some(&3.0));
83
+ // 再 drain → 空。
84
+ let (drained2, _after2) = take_pending(Some(&after));
85
+ assert!(drained2.is_empty());
86
+ // drain None → ([], default state)。
87
+ let (none_drained, none_state) = take_pending(None);
88
+ assert!(none_drained.is_empty());
89
+ assert!(none_state.pending.is_empty());
90
+ }
91
+
92
+ // =====================================================================
93
+ // 5. leader_session_name — sha1 派生 + 文件夹消毒(unimplemented → RED)
94
+ // =====================================================================
95
+
96
+ // 公式:team-agent-leader-<provider>-<sanitized folder[:48]>-<sha1(resolve(ws))[:8]>。
97
+ // 用真实 temp 目录,sha1/sanitize 在测试内复算后断言函数输出与之一致(probe 已验证公式)。
98
+ #[test]
99
+ fn leader_session_name_formula_and_sanitization() {
100
+ // 公式 = team-agent-leader-<provider>-<sanitized folder>-<sha1(resolve(ws))[:8]>。
101
+ // sha1 复算需 sha1 crate(本测试不引);改为断言格式不变量(provider/消毒/8-hex 后缀),
102
+ // 字节级 sha1 由 golden probe_leader.py 已验证公式正确。
103
+ let base = std::env::temp_dir().join(format!("ta_rs_lsn_{}", std::process::id()));
104
+ let weird = base.join("My Proj!@#name");
105
+ std::fs::create_dir_all(&weird).unwrap();
106
+ let got = leader_session_name(Provider::Codex, &weird);
107
+ // 前缀 + provider + 消毒后的 folder(非字母数字_.- → '_')。
108
+ let s = got.as_str();
109
+ assert!(
110
+ s.starts_with("team-agent-leader-codex-My_Proj___name-"),
111
+ "got {s}"
112
+ );
113
+ // sha1[:8] 后缀:8 个 hex。
114
+ let suffix = s.rsplit('-').next().unwrap();
115
+ assert_eq!(suffix.len(), 8, "sha1 前缀须 8 hex,got {suffix}");
116
+ assert!(suffix.chars().all(|c| c.is_ascii_hexdigit()));
117
+ }
118
+
119
+ // folder 名消毒成空 → 回退 "workspace"(probe_leader.py allsym 用例)。
120
+ #[test]
121
+ fn leader_session_name_empty_sanitized_folder_falls_back_to_workspace() {
122
+ let base = std::env::temp_dir().join(format!("ta_rs_lsn2_{}", std::process::id()));
123
+ // 全符号目录名 → 消毒后 strip('._-') 为空 → "workspace"。
124
+ let allsym = base.join("@@@");
125
+ std::fs::create_dir_all(&allsym).unwrap();
126
+ let got = leader_session_name(Provider::Codex, &allsym);
127
+ assert!(
128
+ got.as_str().contains("-workspace-"),
129
+ "全符号 folder 应回退 'workspace',got {}",
130
+ got.as_str()
131
+ );
132
+ }
133
+
134
+ // claude_code provider 出现在 session 名里(probe:team-agent-leader-claude_code-...)。
135
+ #[test]
136
+ fn leader_session_name_uses_claude_code_provider_string() {
137
+ let base = std::env::temp_dir().join(format!("ta_rs_lsn3_{}", std::process::id()));
138
+ let dir = base.join("proj");
139
+ std::fs::create_dir_all(&dir).unwrap();
140
+ let got = leader_session_name(Provider::ClaudeCode, &dir);
141
+ assert!(
142
+ got.as_str()
143
+ .starts_with("team-agent-leader-claude_code-proj-"),
144
+ "got {}",
145
+ got.as_str()
146
+ );
147
+ }
148
+
149
+ #[test]
150
+ fn leader_session_name_uses_copilot_provider_string() {
151
+ let base = std::env::temp_dir().join(format!("ta_rs_lsn_copilot_{}", std::process::id()));
152
+ let dir = base.join("proj");
153
+ std::fs::create_dir_all(&dir).unwrap();
154
+ let got = leader_session_name(Provider::Copilot, &dir);
155
+ assert!(
156
+ got.as_str().starts_with("team-agent-leader-copilot-proj-"),
157
+ "got {}",
158
+ got.as_str()
159
+ );
160
+ }
161
+
162
+ // =====================================================================
163
+ // 6. Family A 正源 owner 绑定 — bind_owner_from_caller_pane(unimplemented → RED)
164
+ // =====================================================================
165
+
166
+ // $TMUX_PANE 缺 → refuse + reason=caller_pane_missing(leader_binding.py:79-95)。
167
+ // 此处只能在 $TMUX_PANE 缺失环境下断言(测试进程通常无 TMUX_PANE)。
168
+ #[test]
169
+ fn bind_owner_refuses_when_caller_pane_missing() {
170
+ // 防御:确保本测试看到的环境无 TMUX_PANE(若 CI 在 tmux 内跑,跳过断言形态)。
171
+ if std::env::var_os("TMUX_PANE").is_some() {
172
+ // 在 tmux 内:正源存在,不该走 refuse 分支;此用例只验缺失分支,直接返回。
173
+ return;
13
174
  }
14
-
15
- #[test]
16
- fn should_reread_never_classified() {
17
- // last_classified_mtime is None → {reread:true, never_classified}。
18
- let d = should_reread(None, Some(10.0), None, 100.0, 60.0);
19
- assert!(d.reread);
20
- assert_eq!(d.reason, RereadReason::NeverClassified);
21
- }
22
-
23
- #[test]
24
- fn should_reread_file_changed() {
25
- // current != last_classified → {reread:true, file_changed}。
26
- let d = should_reread(Some(5.0), Some(20.0), Some(10.0), 100.0, 60.0);
27
- assert!(d.reread);
28
- assert_eq!(d.reason, RereadReason::FileChanged);
29
- }
30
-
31
- #[test]
32
- fn should_reread_quiescent_already_classified_at_and_past_debounce() {
33
- // current==last_classified 且 silent_for >= debounce → quiescent(不再重读)。
34
- // silent_for = max(0, now-current_mtime) = 100-10 = 90 >= 60。
35
- let d = should_reread(Some(10.0), Some(10.0), Some(10.0), 100.0, 60.0);
36
- assert!(!d.reread);
37
- assert_eq!(d.reason, RereadReason::QuiescentAlreadyClassified);
38
- // 边界:silent_for 恰 == debounce(now-current=60)→ 仍 quiescent(>= 比较)。
39
- let b = should_reread(Some(40.0), Some(40.0), Some(40.0), 100.0, 60.0);
40
- assert!(!b.reread);
41
- assert_eq!(b.reason, RereadReason::QuiescentAlreadyClassified);
42
- }
43
-
44
- #[test]
45
- fn should_reread_unchanged_within_debounce() {
46
- // current==last_classified 且 silent_for < debounce → unchanged。
47
- // now-current = 100-95 = 5 < 60。注:last_mtime 在 Python body 中未被使用。
48
- let d = should_reread(Some(10.0), Some(95.0), Some(95.0), 100.0, 60.0);
49
- assert!(!d.reread);
50
- assert_eq!(d.reason, RereadReason::Unchanged);
51
- }
52
-
53
- #[test]
54
- fn on_file_changed_adds_node_sorted_and_records_mtime() {
55
- // golden probe_leader.py:add b → pending ["b"];add a → sorted ["a","b"]。
56
- let s0 = on_file_changed(None, "b", 1.0);
57
- assert_eq!(s0.pending, vec!["b".to_string()]);
58
- assert_eq!(s0.mtimes.get("b"), Some(&1.0));
59
- let s1 = on_file_changed(Some(&s0), "a", 2.0);
60
- assert_eq!(s1.pending, vec!["a".to_string(), "b".to_string()], "pending 必须 sorted");
61
- assert_eq!(s1.mtimes.get("a"), Some(&2.0));
62
- // 重复 add b:pending 仍是 set(不重复),mtime 被更新到 3.0。
63
- let s2 = on_file_changed(Some(&s1), "b", 3.0);
64
- assert_eq!(s2.pending, vec!["a".to_string(), "b".to_string()]);
65
- assert_eq!(s2.mtimes.get("b"), Some(&3.0));
66
- }
67
-
68
- #[test]
69
- fn take_pending_drains_sorted_and_clears_pending_but_keeps_mtimes() {
70
- // golden probe_leader.py:drain → (["a","b"], state{pending:[], mtimes 保留})。
71
- let mut st = on_file_changed(None, "b", 3.0);
72
- st = on_file_changed(Some(&st), "a", 2.0);
73
- let (drained, after) = take_pending(Some(&st));
74
- assert_eq!(drained, vec!["a".to_string(), "b".to_string()]);
75
- assert!(after.pending.is_empty());
76
- // mtimes 不被 drain 清空。
77
- assert_eq!(after.mtimes.get("a"), Some(&2.0));
78
- assert_eq!(after.mtimes.get("b"), Some(&3.0));
79
- // 再 drain → 空。
80
- let (drained2, _after2) = take_pending(Some(&after));
81
- assert!(drained2.is_empty());
82
- // drain None → ([], default state)。
83
- let (none_drained, none_state) = take_pending(None);
84
- assert!(none_drained.is_empty());
85
- assert!(none_state.pending.is_empty());
86
- }
87
-
88
- // =====================================================================
89
- // 5. leader_session_name — sha1 派生 + 文件夹消毒(unimplemented → RED)
90
- // =====================================================================
91
-
92
- // 公式:team-agent-leader-<provider>-<sanitized folder[:48]>-<sha1(resolve(ws))[:8]>。
93
- // 用真实 temp 目录,sha1/sanitize 在测试内复算后断言函数输出与之一致(probe 已验证公式)。
94
- #[test]
95
- fn leader_session_name_formula_and_sanitization() {
96
- // 公式 = team-agent-leader-<provider>-<sanitized folder>-<sha1(resolve(ws))[:8]>。
97
- // sha1 复算需 sha1 crate(本测试不引);改为断言格式不变量(provider/消毒/8-hex 后缀),
98
- // 字节级 sha1 由 golden probe_leader.py 已验证公式正确。
99
- let base = std::env::temp_dir().join(format!("ta_rs_lsn_{}", std::process::id()));
100
- let weird = base.join("My Proj!@#name");
101
- std::fs::create_dir_all(&weird).unwrap();
102
- let got = leader_session_name(Provider::Codex, &weird);
103
- // 前缀 + provider + 消毒后的 folder(非字母数字_.- → '_')。
104
- let s = got.as_str();
105
- assert!(s.starts_with("team-agent-leader-codex-My_Proj___name-"), "got {s}");
106
- // sha1[:8] 后缀:8 个 hex。
107
- let suffix = s.rsplit('-').next().unwrap();
108
- assert_eq!(suffix.len(), 8, "sha1 前缀须 8 hex,got {suffix}");
109
- assert!(suffix.chars().all(|c| c.is_ascii_hexdigit()));
110
- }
111
-
112
- // folder 名消毒成空 → 回退 "workspace"(probe_leader.py allsym 用例)。
113
- #[test]
114
- fn leader_session_name_empty_sanitized_folder_falls_back_to_workspace() {
115
- let base = std::env::temp_dir().join(format!("ta_rs_lsn2_{}", std::process::id()));
116
- // 全符号目录名 → 消毒后 strip('._-') 为空 → "workspace"。
117
- let allsym = base.join("@@@");
118
- std::fs::create_dir_all(&allsym).unwrap();
119
- let got = leader_session_name(Provider::Codex, &allsym);
120
- assert!(
121
- got.as_str().contains("-workspace-"),
122
- "全符号 folder 应回退 'workspace',got {}",
123
- got.as_str()
124
- );
125
- }
126
-
127
- // claude_code provider 出现在 session 名里(probe:team-agent-leader-claude_code-...)。
128
- #[test]
129
- fn leader_session_name_uses_claude_code_provider_string() {
130
- let base = std::env::temp_dir().join(format!("ta_rs_lsn3_{}", std::process::id()));
131
- let dir = base.join("proj");
132
- std::fs::create_dir_all(&dir).unwrap();
133
- let got = leader_session_name(Provider::ClaudeCode, &dir);
134
- assert!(got.as_str().starts_with("team-agent-leader-claude_code-proj-"), "got {}", got.as_str());
135
- }
136
-
137
- #[test]
138
- fn leader_session_name_uses_copilot_provider_string() {
139
- let base = std::env::temp_dir().join(format!("ta_rs_lsn_copilot_{}", std::process::id()));
140
- let dir = base.join("proj");
141
- std::fs::create_dir_all(&dir).unwrap();
142
- let got = leader_session_name(Provider::Copilot, &dir);
143
- assert!(
144
- got.as_str().starts_with("team-agent-leader-copilot-proj-"),
145
- "got {}",
146
- got.as_str()
147
- );
148
- }
149
-
150
- // =====================================================================
151
- // 6. Family A 正源 owner 绑定 — bind_owner_from_caller_pane(unimplemented → RED)
152
- // =====================================================================
153
-
154
- // $TMUX_PANE 缺 → refuse + reason=caller_pane_missing(leader_binding.py:79-95)。
155
- // 此处只能在 $TMUX_PANE 缺失环境下断言(测试进程通常无 TMUX_PANE)。
156
- #[test]
157
- fn bind_owner_refuses_when_caller_pane_missing() {
158
- // 防御:确保本测试看到的环境无 TMUX_PANE(若 CI 在 tmux 内跑,跳过断言形态)。
159
- if std::env::var_os("TMUX_PANE").is_some() {
160
- // 在 tmux 内:正源存在,不该走 refuse 分支;此用例只验缺失分支,直接返回。
161
- return;
162
- }
163
- let ws = std::env::temp_dir().join(format!("ta_rs_bind_{}", std::process::id()));
164
- std::fs::create_dir_all(&ws).unwrap();
165
- let team = TeamKey::new("default");
166
- let res = bind_owner_from_caller_pane(&ws, &team, None).unwrap();
167
- assert!(!res.ok);
168
- assert_eq!(res.reason, Some(LeaseReason::CallerPaneMissing));
169
- // caller_pane_id 为空(probe:""),hint 为 _HINT_RUN_FROM_LEADER_PANE。
170
- assert_eq!(res.caller_pane_id, PaneId::new(""));
171
- assert_eq!(res.caller_current_command, "");
172
- assert_eq!(
175
+ let ws = std::env::temp_dir().join(format!("ta_rs_bind_{}", std::process::id()));
176
+ std::fs::create_dir_all(&ws).unwrap();
177
+ let team = TeamKey::new("default");
178
+ let res = bind_owner_from_caller_pane(&ws, &team, None).unwrap();
179
+ assert!(!res.ok);
180
+ assert_eq!(res.reason, Some(LeaseReason::CallerPaneMissing));
181
+ // caller_pane_id 为空(probe:""),hint 为 _HINT_RUN_FROM_LEADER_PANE。
182
+ assert_eq!(res.caller_pane_id, PaneId::new(""));
183
+ assert_eq!(res.caller_current_command, "");
184
+ assert_eq!(
173
185
  res.hint.as_deref(),
174
186
  Some("run team-agent from inside your leader pane (the tmux pane you want to own this team).")
175
187
  );
176
- assert_eq!(res.team_id, team);
177
- }
178
-
179
- // owner.bind_refused 事件名字节锁(LeaderEvent::name unimplemented → RED;与 #5 重叠但锁 binding 路径)。
180
- #[test]
181
- fn owner_bind_refused_event_name_is_owner_bind_refused() {
182
- assert_eq!(LeaderEvent::OwnerBindRefused.name(), "owner.bind_refused");
183
- }
184
-
185
- // emit_owner_bound_event:成功绑定 hook(owner.bound_from_caller_pane;leader_binding.py:162-183)。
186
- // 强化(no-full-uuid-leak 命门):事件只写 derived_uuid_prefix == derived[:12](12 hex),
187
- // old uuid 为 None → old_uuid_prefix == ""(空串,非缺省);全 32 hex uuid 绝不出现在任何字段。
188
- // unimplemented → RED。
189
- #[test]
190
- fn emit_owner_bound_event_logs_prefix_only_never_full_uuid() {
191
- let ws = std::env::temp_dir().join(format!("ta_rs_emit_{}", std::process::id()));
192
- std::fs::create_dir_all(&ws).unwrap();
193
- let caller = PaneId::new("%7");
194
- let derived = uuid("fp", "/ws", "u", "default");
195
- let full = derived.as_str().to_string();
196
- assert_eq!(full.len(), 32, "derive 产 32 hex");
197
- let prefix12 = full[..12].to_string();
198
- emit_owner_bound_event(&ws, &caller, "codex", &derived, &TeamKey::new("default"), None).unwrap();
199
- // 读回审计事件:恰一条 owner.bound_from_caller_pane。
200
- let events = crate::event_log::EventLog::new(&ws).tail(50).unwrap();
201
- let ev = events
202
- .iter()
203
- .find(|e| e["event"] == serde_json::json!("owner.bound_from_caller_pane"))
204
- .expect("必写 owner.bound_from_caller_pane");
205
- assert_eq!(ev["caller_pane_id"], serde_json::json!("%7"));
206
- assert_eq!(ev["caller_current_command"], serde_json::json!("codex"));
207
- assert_eq!(ev["team_id"], serde_json::json!("default"));
208
- // derived_uuid_prefix == derived[:12](只前缀,12 hex)
209
- assert_eq!(ev["derived_uuid_prefix"], serde_json::json!(prefix12));
210
- // old uuid=None → old_uuid_prefix == ""(空串,非 null/缺省;golden probe 已验)
211
- assert_eq!(ev["old_uuid_prefix"], serde_json::json!(""));
212
- // no-full-uuid-leak:整条事件序列化文本里绝不出现完整 32-hex uuid。
213
- let raw = serde_json::to_string(ev).unwrap();
214
- assert!(!raw.contains(&full), "审计事件绝不泄露完整 leader_session_uuid");
215
- // 审计事件名字节锁。
216
- assert_eq!(LeaderEvent::OwnerBoundFromCallerPane.name(), "owner.bound_from_caller_pane");
217
- }
188
+ assert_eq!(res.team_id, team);
189
+ }
190
+
191
+ // owner.bind_refused 事件名字节锁(LeaderEvent::name unimplemented → RED;与 #5 重叠但锁 binding 路径)。
192
+ #[test]
193
+ fn owner_bind_refused_event_name_is_owner_bind_refused() {
194
+ assert_eq!(LeaderEvent::OwnerBindRefused.name(), "owner.bind_refused");
195
+ }
196
+
197
+ // emit_owner_bound_event:成功绑定 hook(owner.bound_from_caller_pane;leader_binding.py:162-183)。
198
+ // 强化(no-full-uuid-leak 命门):事件只写 derived_uuid_prefix == derived[:12](12 hex),
199
+ // old uuid 为 None → old_uuid_prefix == ""(空串,非缺省);全 32 hex uuid 绝不出现在任何字段。
200
+ // unimplemented → RED。
201
+ #[test]
202
+ fn emit_owner_bound_event_logs_prefix_only_never_full_uuid() {
203
+ let ws = std::env::temp_dir().join(format!("ta_rs_emit_{}", std::process::id()));
204
+ std::fs::create_dir_all(&ws).unwrap();
205
+ let caller = PaneId::new("%7");
206
+ let derived = uuid("fp", "/ws", "u", "default");
207
+ let full = derived.as_str().to_string();
208
+ assert_eq!(full.len(), 32, "derive 产 32 hex");
209
+ let prefix12 = full[..12].to_string();
210
+ emit_owner_bound_event(
211
+ &ws,
212
+ &caller,
213
+ "codex",
214
+ &derived,
215
+ &TeamKey::new("default"),
216
+ None,
217
+ )
218
+ .unwrap();
219
+ // 读回审计事件:恰一条 owner.bound_from_caller_pane。
220
+ let events = crate::event_log::EventLog::new(&ws).tail(50).unwrap();
221
+ let ev = events
222
+ .iter()
223
+ .find(|e| e["event"] == serde_json::json!("owner.bound_from_caller_pane"))
224
+ .expect("必写 owner.bound_from_caller_pane");
225
+ assert_eq!(ev["caller_pane_id"], serde_json::json!("%7"));
226
+ assert_eq!(ev["caller_current_command"], serde_json::json!("codex"));
227
+ assert_eq!(ev["team_id"], serde_json::json!("default"));
228
+ // derived_uuid_prefix == derived[:12](只前缀,12 hex)
229
+ assert_eq!(ev["derived_uuid_prefix"], serde_json::json!(prefix12));
230
+ // old uuid=None → old_uuid_prefix == ""(空串,非 null/缺省;golden probe 已验)。
231
+ assert_eq!(ev["old_uuid_prefix"], serde_json::json!(""));
232
+ // no-full-uuid-leak:整条事件序列化文本里绝不出现完整 32-hex uuid。
233
+ let raw = serde_json::to_string(ev).unwrap();
234
+ assert!(
235
+ !raw.contains(&full),
236
+ "审计事件绝不泄露完整 leader_session_uuid"
237
+ );
238
+ // 审计事件名字节锁。
239
+ assert_eq!(
240
+ LeaderEvent::OwnerBoundFromCallerPane.name(),
241
+ "owner.bound_from_caller_pane"
242
+ );
243
+ }
@@ -69,10 +69,10 @@ pub mod transport;
69
69
  // step 10-12 (leader/messaging/coordinator) — ROUND-0.5 类型+fn surface 骨架(fn body unimplemented!(),
70
70
  // P2 porter 落实现)。leader=lease/owner-bind/idle-takeover;messaging=send/deliver/retry/watchers;
71
71
  // coordinator=daemon tick(§10 no-panic,Result<TickReport,TickError>)。
72
- pub mod leader;
73
- pub mod messaging;
74
72
  pub mod coordinator;
75
73
  pub mod diagnose;
74
+ pub mod leader;
75
+ pub mod messaging;
76
76
 
77
77
  // step 13-15 (lifecycle/mcp_server/cli/packaging) — ROUND-0.5b behavioral-rich 骨架(entry-fn 签名 +
78
78
  // 富返回类型,fn body unimplemented!(),P2 porter 落实现)。lifecycle=quick-start/restart/display;
@@ -87,11 +87,11 @@ pub mod lifecycle;
87
87
  pub mod platform;
88
88
  // 0.3.28 — unified adaptive layout manager (single source of truth for tmux
89
89
  // topology decisions). See `.team/artifacts/adaptive-layout-full-architecture-locate.md`.
90
+ pub mod cli;
90
91
  pub mod layout;
91
- pub mod topology;
92
92
  pub mod mcp_server;
93
- pub mod cli;
94
93
  pub mod packaging;
94
+ pub mod topology;
95
95
 
96
96
  // fake-worker — subscription-free backing program for Provider::Fake; lets the real spawn path
97
97
  // (launch dry_run=false → tmux window) be exercised with no real provider (port of fake_worker.py).
@@ -217,7 +217,10 @@ fn open_adaptive_worker_displays(
217
217
  workspace_window: target.window.clone(),
218
218
  pane_id: target.pane_id.clone(),
219
219
  pane_title: Some(agent_id),
220
- target: target.pane_id.as_ref().map(|pane| pane.as_str().to_string()),
220
+ target: target
221
+ .pane_id
222
+ .as_ref()
223
+ .map(|pane| pane.as_str().to_string()),
221
224
  target_worker_session: Some(session_name.as_str().to_string()),
222
225
  linked_session: None,
223
226
  leader_session: Some(session_name.clone()),
@@ -279,7 +282,9 @@ fn close_adaptive_displays(
279
282
  }
280
283
  }
281
284
  if let Some(leader_session) = adaptive_leader_session(&state) {
282
- for target in close_adaptive_windows(&transport, leader_session.as_str(), session_name.as_str()) {
285
+ for target in
286
+ close_adaptive_windows(&transport, leader_session.as_str(), session_name.as_str())
287
+ {
283
288
  orphans_cleaned.push(target.clone());
284
289
  if seen.insert(target.clone()) {
285
290
  closed.push(target);
@@ -481,4 +486,3 @@ fn string_field(display: &serde_json::Map<String, serde_json::Value>, key: &str)
481
486
  .filter(|s| !s.is_empty())
482
487
  .map(str::to_string)
483
488
  }
484
-
@@ -139,7 +139,12 @@ pub fn launch_with_transport_in_workspace(
139
139
  .map(|id| id == agent.agent_id.as_str())
140
140
  .unwrap_or(false)
141
141
  })
142
- .and_then(|entry| entry.get("provider").and_then(Value::as_str).map(str::to_string))
142
+ .and_then(|entry| {
143
+ entry
144
+ .get("provider")
145
+ .and_then(Value::as_str)
146
+ .map(str::to_string)
147
+ })
143
148
  .unwrap_or_else(|| "fake".to_string());
144
149
  crate::lifecycle::restart::write_worker_spawn_timing_event(
145
150
  workspace,
@@ -371,9 +376,13 @@ fn spawn_agents(
371
376
  apply_mcp_auto_approval_env(&mut env, &safety);
372
377
  // Python providers.py:145 + launch/core.py:253 — fresh launch runs the worker
373
378
  // with cwd=workspace, same as the RS fork/add and restart paths.
374
- let env_unset: Vec<String> = extend_worker_env_unset_for_effort(
375
- profile_launch.env_unset.iter().cloned().collect(),
379
+ let env_unset = crate::layout::worker_env::isolate_worker_spawn_env(
376
380
  provider,
381
+ &mut env,
382
+ extend_worker_env_unset_for_effort(
383
+ profile_launch.env_unset.iter().cloned().collect(),
384
+ provider,
385
+ ),
377
386
  );
378
387
  // BUG / C-1-2 / C-6-1 cr verdict — Copilot system_prompt 走 spawn env overlay +
379
388
  // per-worker AGENTS.md(B2 灵魂件降级):写
@@ -3970,6 +3979,7 @@ fn add_agent_with_transport_at_paths(
3970
3979
  pre_spec_text.as_deref(),
3971
3980
  pre_runtime_state.as_ref(),
3972
3981
  agent_id,
3982
+ "state_upsert_failed",
3973
3983
  );
3974
3984
  return Err(error);
3975
3985
  }
@@ -3991,6 +4001,7 @@ fn add_agent_with_transport_at_paths(
3991
4001
  pre_spec_text.as_deref(),
3992
4002
  pre_runtime_state.as_ref(),
3993
4003
  agent_id,
4004
+ "start_agent_failed",
3994
4005
  );
3995
4006
  return Err(error);
3996
4007
  }
@@ -4007,6 +4018,7 @@ fn add_agent_with_transport_at_paths(
4007
4018
  pre_spec_text.as_deref(),
4008
4019
  pre_runtime_state.as_ref(),
4009
4020
  agent_id,
4021
+ "added_agent_paused",
4010
4022
  );
4011
4023
  return Err(LifecycleError::RequirementUnmet(format!(
4012
4024
  "added agent {agent_id} is paused"
@@ -4032,12 +4044,21 @@ fn rollback_add_agent_atomic(
4032
4044
  pre_spec_text: Option<&str>,
4033
4045
  pre_runtime_state: Option<&serde_json::Value>,
4034
4046
  agent_id: &AgentId,
4047
+ reason: &str,
4035
4048
  ) {
4036
- if let Some(text) = pre_spec_text {
4037
- let _ = std::fs::write(spec_path, text);
4049
+ let spec_restored = if let Some(text) = pre_spec_text {
4050
+ std::fs::write(spec_path, text).is_ok()
4038
4051
  } else {
4039
- let _ = std::fs::remove_file(spec_path);
4040
- }
4052
+ std::fs::remove_file(spec_path)
4053
+ .or_else(|error| {
4054
+ if error.kind() == std::io::ErrorKind::NotFound {
4055
+ Ok(())
4056
+ } else {
4057
+ Err(error)
4058
+ }
4059
+ })
4060
+ .is_ok()
4061
+ };
4041
4062
  // 0.5.26 (`.team/artifacts/stale-team-saveconflict-locate.md` §7.4):
4042
4063
  // rollback must tombstone the newly-added agent so the persist merge
4043
4064
  // does not re-attach a `roster_stub` from the latest on disk. Without
@@ -4045,12 +4066,13 @@ fn rollback_add_agent_atomic(
4045
4066
  // survives the restore-from-pre_state pass and the retry sees
4046
4067
  // "agent id already exists".
4047
4068
  let deleted = [agent_id.as_str()];
4048
- if let Some(state) = pre_runtime_state {
4049
- let _ = crate::state::persist::save_runtime_state_with_deleted_agents(
4069
+ let state_restored = if let Some(state) = pre_runtime_state {
4070
+ crate::state::persist::save_runtime_state_with_deleted_agents(
4050
4071
  run_workspace,
4051
4072
  state,
4052
4073
  &deleted,
4053
- );
4074
+ )
4075
+ .is_ok()
4054
4076
  } else {
4055
4077
  // No prior runtime state — drop just the agent we added (load → strip → save).
4056
4078
  if let Ok(mut state) = crate::state::persist::load_runtime_state(run_workspace) {
@@ -4073,13 +4095,27 @@ fn rollback_add_agent_atomic(
4073
4095
  }
4074
4096
  }
4075
4097
  }
4076
- let _ = crate::state::persist::save_runtime_state_with_deleted_agents(
4098
+ crate::state::persist::save_runtime_state_with_deleted_agents(
4077
4099
  run_workspace,
4078
4100
  &state,
4079
4101
  &deleted,
4080
- );
4102
+ )
4103
+ .is_ok()
4104
+ } else {
4105
+ false
4081
4106
  }
4082
- }
4107
+ };
4108
+ let rollback_ok = spec_restored && state_restored;
4109
+ let _ = crate::event_log::EventLog::new(run_workspace).write(
4110
+ "add_agent.rollback",
4111
+ serde_json::json!({
4112
+ "agent_id": agent_id.as_str(),
4113
+ "reason": reason,
4114
+ "rollback_ok": rollback_ok,
4115
+ "spec_restored": spec_restored,
4116
+ "state_restored": state_restored,
4117
+ }),
4118
+ );
4083
4119
  }
4084
4120
 
4085
4121
  fn upsert_agent_state_from_role(
@@ -4524,9 +4560,13 @@ pub fn fork_agent_with_transport(
4524
4560
  // _tmux_session_exists — an ABSENT session => new-session (spawn_first), present => new-window
4525
4561
  // (spawn_into). The Rust restart seam (restart.rs spawn_agent_window) uses the same branch.
4526
4562
  let session_live = transport.has_session(&session_name).unwrap_or(false);
4527
- let env_unset: Vec<String> = extend_worker_env_unset_for_effort(
4528
- profile_launch.env_unset.iter().cloned().collect(),
4563
+ let env_unset = crate::layout::worker_env::isolate_worker_spawn_env(
4529
4564
  provider,
4565
+ &mut env,
4566
+ extend_worker_env_unset_for_effort(
4567
+ profile_launch.env_unset.iter().cloned().collect(),
4568
+ provider,
4569
+ ),
4530
4570
  );
4531
4571
  let spawn_result = if session_live {
4532
4572
  transport.spawn_into_with_env_unset(