@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
@@ -1,6 +1,30 @@
1
+ //! ---
2
+ //! purpose: fork 一席的入口,解析活跃 team 并绑定它实际使用的 tmux socket
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: fork_agent
6
+ //! what: 解析 team 与 transport 后转 fork_agent_with_transport
7
+ //! depends:
8
+ //! - crate::state::selector
9
+ //! - crate::lifecycle::restart
10
+ //! - crate::tmux_backend
11
+ //! boundary:
12
+ //! - 不自己做窗口注入,注入在 fork_agent.rs
13
+ //! - 不用 workspace 哈希兜底 socket 覆盖已持久化的 endpoint
14
+ //! maturity: wired
15
+ //! ---
1
16
  //!
2
17
  use super::*;
3
18
 
19
+ /// ---
20
+ /// purpose: fork 一席的对外入口
21
+ /// params:
22
+ /// source_agent_id: 源席位
23
+ /// as_agent_id: 新席位名
24
+ /// label: 新席位的角色标签
25
+ /// returns: fork 报告
26
+ /// errors: 选不到活跃 team 返回 TeamSelect,其余透传 fork_agent_with_transport
27
+ /// ---
4
28
  /// `fork_agent(workspace, source_agent_id, as_agent_id, ...)`(`lifecycle/operations.py:284`)。
5
29
  /// native session fork(provider 须 supports_session_fork ∧ auth_mode!=compatible_api);
6
30
  /// 失败回滚,每条失败臂 `adapter.cleanup_mcp`。
@@ -0,0 +1,260 @@
1
+ //! ---
2
+ //! purpose: 判定 .grok/config.toml env 里哪些键是每席私有
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: is_per_seat_env_key
6
+ //! what: TEAM_AGENT_* 除 WORKSPACE 外都是每席键
7
+ //! - name: per_seat_keys_in_toml
8
+ //! what: 扫描 toml env 表里的每席键
9
+ //! - name: strip_per_seat_keys_from_toml
10
+ //! what: 从任意 .env 表删掉每席键,留下非框架键
11
+ //! - name: non_per_seat_env_in_tables
12
+ //! what: 从指定表的 .env 取出非每席键(值原样),供 overlay 保留
13
+ //! boundary:
14
+ //! - 不写盘
15
+ //! - 不数 grok 席位个数
16
+ //! - 剥离结果只含键名,不含键值
17
+ //! maturity: wired
18
+ //! ---
19
+
20
+ /// ---
21
+ /// purpose: 判断某环境键是否属于每席私有
22
+ /// returns: 以 TEAM_AGENT_ 起头且不是 TEAM_AGENT_WORKSPACE 时为 true
23
+ /// ---
24
+ /// Shared identity keys belong on pane env. The only TEAM_AGENT_* key
25
+ /// allowed in the directory-scoped grok toml is WORKSPACE (same cwd).
26
+ pub(crate) fn is_per_seat_env_key(key: &str) -> bool {
27
+ let key = key.trim();
28
+ key.starts_with("TEAM_AGENT_") && key != "TEAM_AGENT_WORKSPACE"
29
+ }
30
+
31
+ /// ---
32
+ /// purpose: 扫出 toml 里所有 env 表中的每席键
33
+ /// returns: 首次出现顺序的键值对,值已去掉引号
34
+ /// ---
35
+ pub(crate) fn per_seat_keys_in_toml(text: &str) -> Vec<(String, String)> {
36
+ let mut in_env_table = false;
37
+ let mut out = Vec::new();
38
+ for line in text.lines() {
39
+ let trimmed = line.trim();
40
+ if trimmed.starts_with('[') && trimmed.ends_with(']') {
41
+ let name = &trimmed[1..trimmed.len() - 1];
42
+ in_env_table = name.ends_with(".env");
43
+ continue;
44
+ }
45
+ if !in_env_table {
46
+ continue;
47
+ }
48
+ let Some((key, rest)) = trimmed.split_once('=') else {
49
+ continue;
50
+ };
51
+ let key = key.trim();
52
+ if !is_per_seat_env_key(key) {
53
+ continue;
54
+ }
55
+ let rest = rest.trim();
56
+ let value = rest
57
+ .strip_prefix('"')
58
+ .and_then(|s| s.strip_suffix('"'))
59
+ .or_else(|| rest.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
60
+ .unwrap_or(rest);
61
+ if out.iter().any(|(existing, _)| existing == key) {
62
+ continue;
63
+ }
64
+ out.push((key.to_string(), value.to_string()));
65
+ }
66
+ out
67
+ }
68
+
69
+ /// ---
70
+ /// purpose: 从所有 env 表里删掉每席键
71
+ /// returns: 删后的全文,以及被删键名的首次出现顺序列表,不含值
72
+ /// ---
73
+ /// Drop per-seat `TEAM_AGENT_*` assignments from any `*.env` table.
74
+ /// Other keys (`TEAM_AGENT_WORKSPACE`, `GROK_FOLDER_TRUST`, user keys) stay.
75
+ /// Returned names are first-seen order, values never included.
76
+ pub(crate) fn strip_per_seat_keys_from_toml(text: &str) -> (String, Vec<String>) {
77
+ let mut in_env_table = false;
78
+ let mut out = String::new();
79
+ let mut removed = Vec::new();
80
+ for line in text.lines() {
81
+ let trimmed = line.trim();
82
+ if trimmed.starts_with('[') && trimmed.ends_with(']') {
83
+ let name = &trimmed[1..trimmed.len() - 1];
84
+ in_env_table = name.ends_with(".env");
85
+ out.push_str(line);
86
+ out.push('\n');
87
+ continue;
88
+ }
89
+ if in_env_table {
90
+ if let Some((key, _)) = trimmed.split_once('=') {
91
+ let key = key.trim();
92
+ if is_per_seat_env_key(key) {
93
+ if !removed.iter().any(|existing| existing == key) {
94
+ removed.push(key.to_string());
95
+ }
96
+ continue;
97
+ }
98
+ }
99
+ }
100
+ out.push_str(line);
101
+ out.push('\n');
102
+ }
103
+ (out, removed)
104
+ }
105
+
106
+ fn toml_env_value(rest: &str) -> String {
107
+ let rest = rest.trim();
108
+ rest.strip_prefix('"')
109
+ .and_then(|s| s.strip_suffix('"'))
110
+ .or_else(|| rest.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
111
+ .unwrap_or(rest)
112
+ .to_string()
113
+ }
114
+
115
+ /// ---
116
+ /// purpose: 取出指定表的 env 段里非每席的键值
117
+ /// params:
118
+ /// tables: 目标表名前缀,只看这些表下的 env 段
119
+ /// returns: 键到值的有序映射,供覆盖写入时保留用户键
120
+ /// ---
121
+ /// Keys in `tables*.env` that are **not** per-seat. Unknown keys must be
122
+ /// kept — detection failure does not authorize deletion.
123
+ pub(crate) fn non_per_seat_env_in_tables(
124
+ text: &str,
125
+ tables: &[&str],
126
+ ) -> std::collections::BTreeMap<String, String> {
127
+ let mut in_target_env = false;
128
+ let mut out = std::collections::BTreeMap::new();
129
+ for line in text.lines() {
130
+ let trimmed = line.trim();
131
+ if trimmed.starts_with('[') && trimmed.ends_with(']') {
132
+ let name = &trimmed[1..trimmed.len() - 1];
133
+ in_target_env = name.ends_with(".env")
134
+ && tables
135
+ .iter()
136
+ .any(|table| name == *table || name.starts_with(&format!("{table}.")));
137
+ continue;
138
+ }
139
+ if !in_target_env {
140
+ continue;
141
+ }
142
+ let Some((key, rest)) = trimmed.split_once('=') else {
143
+ continue;
144
+ };
145
+ let key = key.trim();
146
+ if key.is_empty() || is_per_seat_env_key(key) {
147
+ continue;
148
+ }
149
+ out.entry(key.to_string())
150
+ .or_insert_with(|| toml_env_value(rest));
151
+ }
152
+ out
153
+ }
154
+
155
+ #[cfg(test)]
156
+ mod tests {
157
+ use super::*;
158
+
159
+ #[test]
160
+ fn workspace_is_shared_other_team_agent_keys_are_per_seat() {
161
+ assert!(!is_per_seat_env_key("TEAM_AGENT_WORKSPACE"));
162
+ assert!(is_per_seat_env_key("TEAM_AGENT_ID"));
163
+ assert!(is_per_seat_env_key("TEAM_AGENT_OWNER_TEAM_ID"));
164
+ assert!(is_per_seat_env_key("TEAM_AGENT_AUTH_MODE"));
165
+ assert!(is_per_seat_env_key("TEAM_AGENT_FUTURE_SEAT_KEY"));
166
+ assert!(!is_per_seat_env_key("GROK_FOLDER_TRUST"));
167
+ }
168
+
169
+ #[test]
170
+ fn scan_finds_per_seat_keys_in_env_table_only() {
171
+ let text = r#"
172
+ [mcp_servers.other.env]
173
+ TEAM_AGENT_ID = "ignore-other-server-still-counts"
174
+
175
+ [mcp_servers.team_orchestrator]
176
+ command = "/bin/x"
177
+
178
+ [mcp_servers.team_orchestrator.env]
179
+ TEAM_AGENT_WORKSPACE = "/ws"
180
+ TEAM_AGENT_AUTH_MODE = "impostor"
181
+ "#;
182
+ let keys = per_seat_keys_in_toml(text);
183
+ assert!(
184
+ keys.iter()
185
+ .any(|(k, v)| k == "TEAM_AGENT_AUTH_MODE" && v == "impostor"),
186
+ "must see AUTH; keys={keys:?}"
187
+ );
188
+ assert!(
189
+ keys.iter().any(|(k, _)| k == "TEAM_AGENT_ID"),
190
+ "any env table per-seat key counts; keys={keys:?}"
191
+ );
192
+ assert!(
193
+ !keys.iter().any(|(k, _)| k == "TEAM_AGENT_WORKSPACE"),
194
+ "WORKSPACE is shared; keys={keys:?}"
195
+ );
196
+ }
197
+
198
+ #[test]
199
+ fn strip_drops_only_per_seat_keys_and_returns_names() {
200
+ let text = r#"
201
+ [mcp_servers.keep.env]
202
+ GROK_FOLDER_TRUST = "1"
203
+ TEAM_AGENT_ID = "impostor"
204
+ TEAM_AGENT_WORKSPACE = "/ws"
205
+
206
+ [mcp_servers.keep]
207
+ command = "/bin/keep"
208
+
209
+ [other]
210
+ TEAM_AGENT_AUTH_MODE = "not-in-env-table"
211
+ "#;
212
+ let (cleaned, removed) = strip_per_seat_keys_from_toml(text);
213
+ assert_eq!(removed, vec!["TEAM_AGENT_ID".to_string()]);
214
+ assert!(
215
+ !cleaned.contains("TEAM_AGENT_ID"),
216
+ "per-seat key must leave the env table; cleaned={cleaned}"
217
+ );
218
+ assert!(
219
+ cleaned.contains("GROK_FOLDER_TRUST = \"1\"")
220
+ && cleaned.contains("TEAM_AGENT_WORKSPACE = \"/ws\"")
221
+ && cleaned.contains("TEAM_AGENT_AUTH_MODE = \"not-in-env-table\"")
222
+ && cleaned.contains("command = \"/bin/keep\""),
223
+ "non-per-seat keys and non-env tables stay; cleaned={cleaned}"
224
+ );
225
+ assert!(
226
+ !cleaned.contains("impostor"),
227
+ "stripped values must not be rewritten; cleaned={cleaned}"
228
+ );
229
+ }
230
+
231
+ #[test]
232
+ fn non_per_seat_scan_keeps_unknown_keys_from_named_tables_only() {
233
+ let text = r#"
234
+ [mcp_servers.keep-me.env]
235
+ GROK_FOLDER_TRUST = "other-table"
236
+
237
+ [mcp_servers.team_orchestrator.env]
238
+ TEAM_AGENT_ID = "stale"
239
+ GROK_FOLDER_TRUST = "1"
240
+ TEAM_AGENT_WORKSPACE = "/ws"
241
+ USER_EXTRA = "keep"
242
+ "#;
243
+ let kept = non_per_seat_env_in_tables(text, &["mcp_servers.team_orchestrator"]);
244
+ assert_eq!(kept.get("GROK_FOLDER_TRUST").map(String::as_str), Some("1"));
245
+ assert_eq!(kept.get("USER_EXTRA").map(String::as_str), Some("keep"));
246
+ assert_eq!(
247
+ kept.get("TEAM_AGENT_WORKSPACE").map(String::as_str),
248
+ Some("/ws")
249
+ );
250
+ assert!(
251
+ !kept.contains_key("TEAM_AGENT_ID"),
252
+ "per-seat keys are not preserved; kept={kept:?}"
253
+ );
254
+ assert_ne!(
255
+ kept.get("GROK_FOLDER_TRUST").map(String::as_str),
256
+ Some("other-table"),
257
+ "only the named table is read; kept={kept:?}"
258
+ );
259
+ }
260
+ }
@@ -1,3 +1,31 @@
1
+ //! ---
2
+ //! purpose: team 与席位的身份取值,团队键、display 后端、auth_mode、effort 与 quick-start 层级判定
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: spec_team_id
6
+ //! what: 从 spec 里取 team 标识
7
+ //! - name: runtime_team_key_for_spec
8
+ //! what: 由 spec 路径与 session 名算出 runtime 团队键
9
+ //! - name: transport_has_session
10
+ //! what: 探测 tmux session 是否存在,探测本身出错一律当不存在
11
+ //! - name: provider_effort_from_raw
12
+ //! what: 解析 effort 并要求该 provider 支持,否则 None
13
+ //! - name: quick_start_depth_guard
14
+ //! what: 定出本次 quick-start 的父团队与层级,推不出父团队时拒绝
15
+ //! - name: annotate_persisted_team_depth
16
+ //! what: 把父团队与层级写回已落盘的 runtime state
17
+ //! depends:
18
+ //! - crate::state::projection
19
+ //! - crate::state::persist
20
+ //! - crate::state::repository
21
+ //! - crate::lifecycle::display
22
+ //! - crate::provider::wire
23
+ //! boundary:
24
+ //! - 只做身份判定与取值,不 spawn、不开显示
25
+ //! - effort 不被支持时只丢弃并给出事件载荷,不改写成别的档位
26
+ //! - 层级判定拿不准时报错,不默认当顶层团队
27
+ //! maturity: wired
28
+ //! ---
1
29
  use std::collections::{BTreeMap, BTreeSet};
2
30
  use std::path::{Path, PathBuf};
3
31
  use std::process::Command;
@@ -14,6 +42,12 @@ use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest}
14
42
 
15
43
  use super::*;
16
44
 
45
+ /// ---
46
+ /// purpose: 从 spec 里取 team 标识
47
+ /// params:
48
+ /// spec: 已解析的 spec
49
+ /// returns: team.id 优先,其次 team.name,再次顶层 name;都没有则 None
50
+ /// ---
17
51
  pub(super) fn spec_team_id(spec: &Value) -> Option<String> {
18
52
  spec.get("team")
19
53
  .and_then(|v| v.get("id").or_else(|| v.get("name")))
@@ -22,6 +56,10 @@ pub(super) fn spec_team_id(spec: &Value) -> Option<String> {
22
56
  .or_else(|| spec.get("name").and_then(Value::as_str).map(str::to_string))
23
57
  }
24
58
 
59
+ /// ---
60
+ /// purpose: 取 runtime state 里显式记录的当前活跃团队键
61
+ /// returns: 非空的 active_team_key,缺失或为空则 None
62
+ /// ---
25
63
  pub(super) fn explicit_active_team_key(state: &serde_json::Value) -> Option<String> {
26
64
  state
27
65
  .get("active_team_key")
@@ -30,6 +68,10 @@ pub(super) fn explicit_active_team_key(state: &serde_json::Value) -> Option<Stri
30
68
  .map(str::to_string)
31
69
  }
32
70
 
71
+ /// ---
72
+ /// purpose: 由 spec 路径、spec 内容与 session 名拼出 runtime 团队键
73
+ /// returns: 交给 state::projection 计算出的团队键
74
+ /// ---
33
75
  pub(super) fn runtime_team_key_for_spec(
34
76
  spec_path: &Path,
35
77
  spec: &Value,
@@ -45,6 +87,10 @@ pub(super) fn runtime_team_key_for_spec(
45
87
  crate::state::projection::team_state_key(&state)
46
88
  }
47
89
 
90
+ /// ---
91
+ /// purpose: 探测某 tmux session 是否存在
92
+ /// returns: 只有确定存在才返回 true;transport 报错或 panic 一律当作不存在
93
+ /// ---
48
94
  pub(super) fn transport_has_session(transport: &dyn Transport, session_name: &SessionName) -> bool {
49
95
  match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
50
96
  transport.has_session(session_name)
@@ -54,6 +100,10 @@ pub(super) fn transport_has_session(transport: &dyn Transport, session_name: &Se
54
100
  }
55
101
  }
56
102
 
103
+ /// ---
104
+ /// purpose: 取 spec 请求的显示后端并交给 display 解析
105
+ /// returns: 解析后的后端,spec 未写或写了未知值时为默认 adaptive
106
+ /// ---
57
107
  pub(super) fn spec_display_backend(spec: &Value) -> DisplayBackend {
58
108
  let requested = spec
59
109
  .get("runtime")
@@ -67,6 +117,12 @@ pub(super) fn spec_display_backend(spec: &Value) -> DisplayBackend {
67
117
 
68
118
  use crate::provider::wire::parse_provider;
69
119
 
120
+ /// ---
121
+ /// purpose: 把 auth_mode 字符串解析成枚举
122
+ /// params:
123
+ /// raw: 只接受 subscription、official_api、compatible_api
124
+ /// returns: 对应枚举,未知取值返回 None
125
+ /// ---
70
126
  pub(super) fn parse_auth_mode(raw: &str) -> Option<AuthMode> {
71
127
  match raw {
72
128
  "subscription" => Some(AuthMode::Subscription),
@@ -76,6 +132,12 @@ pub(super) fn parse_auth_mode(raw: &str) -> Option<AuthMode> {
76
132
  }
77
133
  }
78
134
 
135
+ /// ---
136
+ /// purpose: 从原始字符串解析 effort,并要求当前 provider 支持它
137
+ /// params:
138
+ /// raw: effort 原文,None 或空白返回 None
139
+ /// returns: 解析成功且该 provider 支持时返回该档位,否则 None
140
+ /// ---
79
141
  /// 0.4.x provider effort MVP step 4: low-level from a raw string. Returns
80
142
  /// `Some(effort)` when the level parses AND the provider supports it.
81
143
  pub(crate) fn provider_effort_from_raw(
@@ -94,6 +156,10 @@ pub(crate) fn provider_effort_from_raw(
94
156
  }
95
157
  }
96
158
 
159
+ /// ---
160
+ /// purpose: 当 spec 请求了该 provider 不支持的 effort 时,给出告警事件载荷
161
+ /// returns: 含 agent_id、provider、effort 与 ignored 动作的 JSON;未请求或本就支持时为 None
162
+ /// ---
97
163
  /// 0.4.x provider effort MVP step 7: warning event payload when the spec
98
164
  /// requested an effort level the provider does not support.
99
165
  pub(crate) fn provider_effort_event_payload(
@@ -118,6 +184,12 @@ pub(crate) fn provider_effort_event_payload(
118
184
  }))
119
185
  }
120
186
 
187
+ /// ---
188
+ /// purpose: 对 claude 系 provider 兜底确保 CLAUDE_EFFORT 进入待清除环境变量表
189
+ /// params:
190
+ /// base: 已有的待清除变量名列表
191
+ /// returns: 非 claude 系原样返回;claude 系在缺失时补上 CLAUDE_EFFORT
192
+ /// ---
121
193
  /// 0.4.x provider effort MVP step 9: defensive guarantee that `CLAUDE_EFFORT`
122
194
  /// is unset in the Claude/ClaudeCode worker spawn env. As of the
123
195
  /// `profile_launch::provider_env_unsets` update, the base list already
@@ -141,6 +213,11 @@ pub(crate) fn extend_worker_env_unset_for_effort(
141
213
  out
142
214
  }
143
215
 
216
+ /// ---
217
+ /// purpose: 从 YAML agent 节点解析出可用的 effort
218
+ /// returns: 该 provider 支持的 effort,否则 None
219
+ /// contract_id: lifecycle.identity.effort_for_spawn
220
+ /// ---
144
221
  /// Convenience: resolve effort for a yaml::Value agent (spec / compiled).
145
222
  pub(crate) fn provider_effort_for_spawn(
146
223
  agent: &crate::model::yaml::Value,
@@ -149,6 +226,11 @@ pub(crate) fn provider_effort_for_spawn(
149
226
  provider_effort_from_raw(agent.get("effort").and_then(|v| v.as_str()), provider)
150
227
  }
151
228
 
229
+ /// ---
230
+ /// purpose: 从 YAML agent 节点取 effort,若被丢弃则给出告警事件载荷
231
+ /// returns: 告警载荷或 None
232
+ /// contract_id: lifecycle.identity.effort_event_if_dropped
233
+ /// ---
152
234
  pub(crate) fn provider_effort_event_if_dropped(
153
235
  agent: &crate::model::yaml::Value,
154
236
  provider: Provider,
@@ -161,6 +243,11 @@ pub(crate) fn provider_effort_event_if_dropped(
161
243
  )
162
244
  }
163
245
 
246
+ /// ---
247
+ /// purpose: 从 runtime state 的 JSON agent 节点解析出可用的 effort
248
+ /// returns: 该 provider 支持的 effort,否则 None
249
+ /// contract_id: lifecycle.identity.effort_for_spawn
250
+ /// ---
164
251
  /// Same as [`provider_effort_for_spawn`] but for serde_json state values
165
252
  /// (used by restart paths reading from `state.agents[id]`).
166
253
  pub(crate) fn provider_effort_for_spawn_json(
@@ -173,6 +260,11 @@ pub(crate) fn provider_effort_for_spawn_json(
173
260
  )
174
261
  }
175
262
 
263
+ /// ---
264
+ /// purpose: 从 JSON agent 节点取 effort,若被丢弃则给出告警事件载荷
265
+ /// returns: 告警载荷或 None
266
+ /// contract_id: lifecycle.identity.effort_event_if_dropped
267
+ /// ---
176
268
  pub(crate) fn provider_effort_event_if_dropped_json(
177
269
  agent: &serde_json::Value,
178
270
  provider: Provider,
@@ -185,6 +277,10 @@ pub(crate) fn provider_effort_event_if_dropped_json(
185
277
  )
186
278
  }
187
279
 
280
+ /// ---
281
+ /// purpose: 定出 quick-start 本次请求的团队键
282
+ /// returns: team_id 优先于 name 的第一个非空值
283
+ /// ---
188
284
  pub(super) fn quick_start_requested_team_key<'a>(
189
285
  team_id: Option<&'a str>,
190
286
  name: Option<&'a str>,
@@ -197,6 +293,15 @@ pub(super) struct QuickStartDepth {
197
293
  pub(super) team_depth: u64,
198
294
  }
199
295
 
296
+ /// ---
297
+ /// purpose: 定出本次 quick-start 的父团队键与团队层级
298
+ /// params:
299
+ /// requested_team: 请求的团队键,用于识别 child 或 grandchild 这类嵌套意图
300
+ /// _agents_dir: 未参与判定
301
+ /// _strict_real_runtime: 未参与判定
302
+ /// returns: 父团队键与层级;无父上下文时父键为 None、层级为 1
303
+ /// errors: 已有活跃团队且请求键像嵌套意图但推不出父团队时返回 RequirementUnmet
304
+ /// ---
200
305
  pub(super) fn quick_start_depth_guard(
201
306
  workspace: &Path,
202
307
  _agents_dir: &Path,
@@ -257,6 +362,10 @@ pub(super) fn quick_start_depth_guard(
257
362
  })
258
363
  }
259
364
 
365
+ /// ---
366
+ /// purpose: 从 runtime state 推断父团队键
367
+ /// returns: 活跃团队键,且该团队确有 running 席位时返回它,否则 None
368
+ /// ---
260
369
  pub(super) fn infer_parent_team_from_active_state(state: &serde_json::Value) -> Option<String> {
261
370
  let active = explicit_active_team_key(state)?;
262
371
  let team = state
@@ -266,6 +375,10 @@ pub(super) fn infer_parent_team_from_active_state(state: &serde_json::Value) ->
266
375
  team_has_running_agent(team).then_some(active)
267
376
  }
268
377
 
378
+ /// ---
379
+ /// purpose: 判断 state 里是否存在带 running 席位的团队
380
+ /// returns: 任一团队有 running 席位即 true
381
+ /// ---
269
382
  pub(super) fn has_live_runtime_teams(state: &serde_json::Value) -> bool {
270
383
  state
271
384
  .get("teams")
@@ -273,6 +386,10 @@ pub(super) fn has_live_runtime_teams(state: &serde_json::Value) -> bool {
273
386
  .is_some_and(|teams| teams.values().any(team_has_running_agent))
274
387
  }
275
388
 
389
+ /// ---
390
+ /// purpose: 判断某团队节点里是否有状态为 running 的席位
391
+ /// returns: 有则 true
392
+ /// ---
276
393
  pub(super) fn team_has_running_agent(team: &serde_json::Value) -> bool {
277
394
  team.get("agents")
278
395
  .and_then(serde_json::Value::as_object)
@@ -283,6 +400,12 @@ pub(super) fn team_has_running_agent(team: &serde_json::Value) -> bool {
283
400
  })
284
401
  }
285
402
 
403
+ /// ---
404
+ /// purpose: 判断团队键是否像有歧义的子团队名
405
+ /// params:
406
+ /// team: 团队键,比较前去空白并转小写
407
+ /// returns: 以 child 开头但不等于 child 时为 true
408
+ /// ---
286
409
  pub(super) fn looks_ambiguous_child_team_key(team: &str) -> bool {
287
410
  let team = team.trim().to_ascii_lowercase();
288
411
  team != "child"
@@ -292,6 +415,10 @@ pub(super) fn looks_ambiguous_child_team_key(team: &str) -> bool {
292
415
  || team.starts_with("child"))
293
416
  }
294
417
 
418
+ /// ---
419
+ /// purpose: 判断团队键是否像孙团队名
420
+ /// returns: 等于 grandchild 或以 grandchild 开头时为 true
421
+ /// ---
295
422
  pub(super) fn looks_grandchild_team_key(team: &str) -> bool {
296
423
  let team = team.trim().to_ascii_lowercase();
297
424
  team == "grandchild"
@@ -301,6 +428,12 @@ pub(super) fn looks_grandchild_team_key(team: &str) -> bool {
301
428
  || team.starts_with("grandchild")
302
429
  }
303
430
 
431
+ /// ---
432
+ /// purpose: 就地给团队节点写上 team_depth 与可选的 parent_team_key
433
+ /// params:
434
+ /// state: 待改写的团队节点,非对象时不动
435
+ /// parent_team_key: 非空时才写入
436
+ /// ---
304
437
  pub(super) fn annotate_team_depth(
305
438
  state: &mut serde_json::Value,
306
439
  parent_team_key: Option<&str>,
@@ -315,6 +448,11 @@ pub(super) fn annotate_team_depth(
315
448
  }
316
449
  }
317
450
 
451
+ /// ---
452
+ /// purpose: 读出 runtime state,给指定团队写上层级与父键后存回
453
+ /// returns: 成功返回空值;state 里没有该团队时直接返回成功且不写盘
454
+ /// errors: 读或写 runtime state 失败时返回 StatePersist
455
+ /// ---
318
456
  pub(super) fn annotate_persisted_team_depth(
319
457
  workspace: &Path,
320
458
  team_key: &str,
@@ -340,6 +478,10 @@ pub(super) fn annotate_persisted_team_depth(
340
478
  Ok(())
341
479
  }
342
480
 
481
+ /// ---
482
+ /// purpose: 判断 runtime state 里是否已存在该 quick-start 团队
483
+ /// returns: 活跃键、teams 表、投影出的团队键、身份字段或 session 名任一命中即 true
484
+ /// ---
343
485
  pub(super) fn runtime_state_has_quick_start_team(state: &serde_json::Value, team: &str) -> bool {
344
486
  explicit_active_team_key(state).as_deref() == Some(team)
345
487
  || state
@@ -359,6 +501,10 @@ pub(super) fn runtime_state_has_quick_start_team(state: &serde_json::Value, team
359
501
  .is_some_and(|session| session == team || session.strip_prefix("team-") == Some(team))
360
502
  }
361
503
 
504
+ /// ---
505
+ /// purpose: 判断某 JSON 节点的团队身份字段是否等于给定团队键
506
+ /// returns: team.id、team.name 或顶层 name 命中即 true
507
+ /// ---
362
508
  pub(super) fn json_team_identity_matches(state: &serde_json::Value, team: &str) -> bool {
363
509
  state
364
510
  .get("team")