@team-agent/installer 0.5.50 → 0.5.51

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 (84) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +5 -3
  4. package/crates/team-agent/src/cli/emit.rs +2 -1
  5. package/crates/team-agent/src/cli/mod.rs +32 -14
  6. package/crates/team-agent/src/cli/named_address.rs +11 -48
  7. package/crates/team-agent/src/cli/send/coordinator.rs +163 -0
  8. package/crates/team-agent/src/cli/send/mailbox.rs +99 -0
  9. package/crates/team-agent/src/cli/send/persist.rs +154 -0
  10. package/crates/team-agent/src/cli/send/presentation.rs +333 -0
  11. package/crates/team-agent/src/cli/send/resolve.rs +361 -0
  12. package/crates/team-agent/src/cli/send.rs +44 -1169
  13. package/crates/team-agent/src/cli/spec.rs +1 -1
  14. package/crates/team-agent/src/cli/status_port/agents.rs +358 -0
  15. package/crates/team-agent/src/cli/status_port/approvals.rs +79 -0
  16. package/crates/team-agent/src/cli/status_port/compact.rs +207 -0
  17. package/crates/team-agent/src/cli/status_port/format.rs +145 -0
  18. package/crates/team-agent/src/cli/status_port/inbox.rs +36 -0
  19. package/crates/team-agent/src/cli/status_port/runtime.rs +195 -0
  20. package/crates/team-agent/src/cli/status_port/snapshot.rs +181 -0
  21. package/crates/team-agent/src/cli/status_port/store.rs +412 -0
  22. package/crates/team-agent/src/cli/status_port/tests.rs +54 -0
  23. package/crates/team-agent/src/cli/status_port.rs +47 -1548
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -0
  25. package/crates/team-agent/src/cli/types.rs +1 -0
  26. package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
  27. package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
  28. package/crates/team-agent/src/coordinator/tick.rs +37 -0
  29. package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
  30. package/crates/team-agent/src/db/message_store.rs +154 -44
  31. package/crates/team-agent/src/event_log.rs +73 -0
  32. package/crates/team-agent/src/leader/start.rs +28 -4
  33. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
  34. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +297 -0
  35. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
  36. package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
  37. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
  38. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
  39. package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
  40. package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
  41. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
  42. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
  43. package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
  44. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
  45. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
  46. package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
  47. package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
  48. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
  49. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
  50. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
  51. package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
  52. package/crates/team-agent/src/lifecycle/restart/agent.rs +44 -26
  53. package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
  54. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +67 -26
  55. package/crates/team-agent/src/lifecycle/restart/remove.rs +435 -72
  56. package/crates/team-agent/src/lifecycle/restart.rs +1 -1
  57. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +575 -17
  58. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +55 -2
  59. package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
  60. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
  61. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
  62. package/crates/team-agent/src/mcp_server/mod.rs +1 -1
  63. package/crates/team-agent/src/mcp_server/tests/send.rs +6 -10
  64. package/crates/team-agent/src/mcp_server/tools.rs +14 -5
  65. package/crates/team-agent/src/messaging/activity.rs +4 -2
  66. package/crates/team-agent/src/messaging/address.rs +86 -0
  67. package/crates/team-agent/src/messaging/delivery.rs +165 -39
  68. package/crates/team-agent/src/messaging/helpers.rs +17 -13
  69. package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
  70. package/crates/team-agent/src/messaging/mod.rs +10 -2
  71. package/crates/team-agent/src/messaging/persist.rs +309 -0
  72. package/crates/team-agent/src/messaging/results.rs +16 -24
  73. package/crates/team-agent/src/messaging/scheduler.rs +4 -2
  74. package/crates/team-agent/src/messaging/selftest.rs +19 -12
  75. package/crates/team-agent/src/messaging/send.rs +101 -49
  76. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
  77. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  78. package/crates/team-agent/src/messaging/tests/runtime.rs +38 -17
  79. package/crates/team-agent/src/messaging/watchers.rs +13 -3
  80. package/crates/team-agent/src/redaction.rs +72 -2
  81. package/crates/team-agent/src/state/persist.rs +2 -1
  82. package/crates/team-agent/src/state/repository/tests.rs +47 -0
  83. package/crates/team-agent/src/state/repository.rs +59 -16
  84. package/package.json +4 -4
@@ -0,0 +1,297 @@
1
+ use std::collections::{BTreeMap, BTreeSet};
2
+ use std::path::{Path, PathBuf};
3
+ use std::process::Command;
4
+
5
+ use crate::lifecycle::*;
6
+ use crate::model::enums::{AuthMode, DisplayBackend, PaneLiveness, Provider, ProviderEffort};
7
+ use crate::model::ids::AgentId;
8
+ use crate::model::permissions::{self, AgentPermissionInput};
9
+ use crate::model::yaml::{self, Value};
10
+ use crate::state::persist::load_runtime_state;
11
+ use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
12
+
13
+ use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
14
+
15
+ use super::*;
16
+
17
+ pub(super) fn maybe_fail_fork_after_spawn(step: &str) -> Result<(), LifecycleError> {
18
+ let Ok(reason) = std::env::var("TEAM_AGENT_TEST_FAIL_FORK_AFTER_SPAWN") else {
19
+ return Ok(());
20
+ };
21
+ if reason.is_empty() {
22
+ return Ok(());
23
+ }
24
+ let should_fail = reason == step || (step == "start_coordinator" && reason == "coordinator");
25
+ if !should_fail {
26
+ return Ok(());
27
+ }
28
+ Err(LifecycleError::StatePersist(format!(
29
+ "injected fork failure after spawn: {reason}"
30
+ )))
31
+ }
32
+
33
+ pub(super) fn cleanup_fork_mcp_artifacts(
34
+ workspace: &Path,
35
+ agent_id: &AgentId,
36
+ mcp_config_path: &Path,
37
+ profile_launch: &crate::provider::ProviderProfileLaunch,
38
+ ) {
39
+ let _ = std::fs::remove_file(mcp_config_path);
40
+ let _ = std::fs::remove_file(
41
+ workspace
42
+ .join(".team/runtime/provider-env")
43
+ .join(format!("{}.env", agent_id.as_str())),
44
+ );
45
+ if let Some(config_dir) = profile_launch.claude_config_dir.as_ref() {
46
+ let _ = std::fs::remove_dir_all(config_dir.parent().unwrap_or(config_dir));
47
+ }
48
+ }
49
+
50
+ pub(super) fn leader_id_matches(spec: &Value, agent_id: &AgentId) -> bool {
51
+ spec.get("leader")
52
+ .and_then(|v| v.get("id"))
53
+ .and_then(Value::as_str)
54
+ .map(|id| id == agent_id.as_str())
55
+ .unwrap_or(false)
56
+ }
57
+
58
+ pub(super) fn find_spec_agent<'a>(spec: &'a Value, agent_id: &AgentId) -> Option<&'a Value> {
59
+ let leader_is_agent = spec
60
+ .get("leader")
61
+ .and_then(|v| v.get("id"))
62
+ .and_then(Value::as_str)
63
+ .map(|id| id == agent_id.as_str())
64
+ .unwrap_or(false);
65
+ if leader_is_agent {
66
+ return None;
67
+ }
68
+ spec.get("agents")?.as_list()?.iter().find(|agent| {
69
+ agent
70
+ .get("id")
71
+ .and_then(Value::as_str)
72
+ .map(|id| id == agent_id.as_str())
73
+ .unwrap_or(false)
74
+ })
75
+ }
76
+
77
+ pub(super) fn append_forked_agent(
78
+ spec: &Value,
79
+ source_agent: &Value,
80
+ source_agent_id: &AgentId,
81
+ as_agent_id: &AgentId,
82
+ label: Option<&str>,
83
+ ) -> Result<Value, LifecycleError> {
84
+ let mut new_agent = source_agent.clone();
85
+ set_yaml_map_value(
86
+ &mut new_agent,
87
+ "id",
88
+ Value::Str(as_agent_id.as_str().to_string()),
89
+ )?;
90
+ // golden operations.py:315 `str(label or new_agent.get("role") or as_agent_id)` — Python `or`
91
+ // falsiness: an EMPTY-string label/role is falsy and falls through to the next tier.
92
+ // The label IS the forked agent's new role (it feeds the identity prompt — B2 family).
93
+ let role = label
94
+ .filter(|s| !s.is_empty())
95
+ .or_else(|| {
96
+ new_agent
97
+ .get("role")
98
+ .and_then(Value::as_str)
99
+ .filter(|s| !s.is_empty())
100
+ })
101
+ .unwrap_or_else(|| as_agent_id.as_str())
102
+ .to_string();
103
+ set_yaml_map_value(&mut new_agent, "role", Value::Str(role.clone()))?;
104
+ set_yaml_map_value(
105
+ &mut new_agent,
106
+ "forked_from",
107
+ Value::Str(source_agent_id.as_str().to_string()),
108
+ )?;
109
+ set_yaml_map_value(
110
+ &mut new_agent,
111
+ "preferred_for",
112
+ Value::List(vec![
113
+ Value::Str(as_agent_id.as_str().to_string()),
114
+ Value::Str(role),
115
+ ]),
116
+ )?;
117
+
118
+ let Value::Map(pairs) = spec else {
119
+ return Err(LifecycleError::Compile(
120
+ "spec root is not a map".to_string(),
121
+ ));
122
+ };
123
+ let mut out = Vec::new();
124
+ for (key, value) in pairs {
125
+ if key == "agents" {
126
+ let mut agents = value
127
+ .as_list()
128
+ .map(|items| items.to_vec())
129
+ .unwrap_or_default();
130
+ agents.push(new_agent.clone());
131
+ out.push((key.clone(), Value::List(agents)));
132
+ } else if key == "runtime" {
133
+ out.push((key.clone(), runtime_with_startup_agent(value, as_agent_id)));
134
+ } else {
135
+ out.push((key.clone(), value.clone()));
136
+ }
137
+ }
138
+ Ok(Value::Map(out))
139
+ }
140
+
141
+ pub(super) fn set_yaml_map_value(
142
+ value: &mut Value,
143
+ key: &str,
144
+ next: Value,
145
+ ) -> Result<(), LifecycleError> {
146
+ let Value::Map(pairs) = value else {
147
+ return Err(LifecycleError::Compile(
148
+ "agent entry is not a map".to_string(),
149
+ ));
150
+ };
151
+ if let Some((_, existing)) = pairs.iter_mut().find(|(k, _)| k == key) {
152
+ *existing = next;
153
+ } else {
154
+ pairs.push((key.to_string(), next));
155
+ }
156
+ Ok(())
157
+ }
158
+
159
+ pub(super) fn runtime_with_startup_agent(runtime: &Value, agent_id: &AgentId) -> Value {
160
+ let Value::Map(pairs) = runtime else {
161
+ return runtime.clone();
162
+ };
163
+ let mut out = Vec::new();
164
+ let mut saw_startup = false;
165
+ for (key, value) in pairs {
166
+ if key == "startup_order" {
167
+ saw_startup = true;
168
+ let mut order = value
169
+ .as_list()
170
+ .map(|items| items.to_vec())
171
+ .unwrap_or_default();
172
+ let already_present = order.iter().any(|item| {
173
+ item.as_str()
174
+ .map(|id| id == agent_id.as_str())
175
+ .unwrap_or(false)
176
+ });
177
+ if !already_present {
178
+ order.push(Value::Str(agent_id.as_str().to_string()));
179
+ }
180
+ out.push((key.clone(), Value::List(order)));
181
+ } else {
182
+ out.push((key.clone(), value.clone()));
183
+ }
184
+ }
185
+ if !saw_startup {
186
+ out.push((
187
+ "startup_order".to_string(),
188
+ Value::List(vec![Value::Str(agent_id.as_str().to_string())]),
189
+ ));
190
+ }
191
+ Value::Map(out)
192
+ }
193
+
194
+ pub(super) fn upsert_forked_agent_state(
195
+ state: &mut serde_json::Value,
196
+ source_agent_id: &AgentId,
197
+ as_agent_id: &AgentId,
198
+ spec_agent: &Value,
199
+ safety: &DangerousApproval,
200
+ plan: &crate::provider::CommandPlan,
201
+ profile_launch: &crate::provider::ProviderProfileLaunch,
202
+ spawn: &crate::transport::SpawnResult,
203
+ spawn_cwd: &Path,
204
+ profile_dir: Option<&Path>,
205
+ ) -> Result<(), LifecycleError> {
206
+ if !state.is_object() {
207
+ *state = serde_json::json!({});
208
+ }
209
+ let Some(root) = state.as_object_mut() else {
210
+ return Err(LifecycleError::StatePersist(
211
+ "runtime state root is not an object".to_string(),
212
+ ));
213
+ };
214
+ let agents = root
215
+ .entry("agents".to_string())
216
+ .or_insert_with(|| serde_json::json!({}));
217
+ if !agents.is_object() {
218
+ *agents = serde_json::json!({});
219
+ }
220
+ let Some(agent_map) = agents.as_object_mut() else {
221
+ return Err(LifecycleError::StatePersist(
222
+ "runtime state agents is not an object".to_string(),
223
+ ));
224
+ };
225
+ let provider = spec_agent
226
+ .get("provider")
227
+ .and_then(Value::as_str)
228
+ .unwrap_or("codex");
229
+ let mut entry = serde_json::Map::new();
230
+ entry.insert("status".to_string(), serde_json::json!("running"));
231
+ entry.insert("provider".to_string(), serde_json::json!(provider));
232
+ entry.insert(
233
+ "agent_id".to_string(),
234
+ serde_json::json!(as_agent_id.as_str()),
235
+ );
236
+ entry.insert(
237
+ "window".to_string(),
238
+ serde_json::json!(as_agent_id.as_str()),
239
+ );
240
+ entry.insert(
241
+ "forked_from".to_string(),
242
+ serde_json::json!(source_agent_id.as_str()),
243
+ );
244
+ entry.insert(
245
+ "spawn_cwd".to_string(),
246
+ serde_json::json!(spawn_cwd.to_string_lossy().to_string()),
247
+ );
248
+ entry.insert(
249
+ "pane_id".to_string(),
250
+ serde_json::json!(spawn.pane_id.as_str()),
251
+ );
252
+ if let Some(pid) = spawn.child_pid {
253
+ entry.insert("pane_pid".to_string(), serde_json::json!(pid));
254
+ }
255
+ for key in [
256
+ "auth_mode",
257
+ "model",
258
+ "model_source",
259
+ "profile",
260
+ "_profile_dir",
261
+ "role",
262
+ // 0.4.x provider effort MVP step 8: fork inherits compiled effort.
263
+ "effort",
264
+ ] {
265
+ if let Some(value) = spec_agent.get(key) {
266
+ entry.insert(key.to_string(), yaml_value_to_json(value));
267
+ }
268
+ }
269
+ if spec_agent.get("profile").is_some() && !entry.contains_key("_profile_dir") {
270
+ if let Some(profile_dir) = profile_dir {
271
+ entry.insert(
272
+ "_profile_dir".to_string(),
273
+ serde_json::json!(profile_dir.to_string_lossy().to_string()),
274
+ );
275
+ }
276
+ }
277
+ entry.insert("session_id".to_string(), serde_json::Value::Null);
278
+ entry.insert("rollout_path".to_string(), serde_json::Value::Null);
279
+ entry.insert("captured_at".to_string(), serde_json::Value::Null);
280
+ entry.insert("captured_via".to_string(), serde_json::Value::Null);
281
+ entry.insert(
282
+ "attribution_confidence".to_string(),
283
+ serde_json::Value::Null,
284
+ );
285
+ persist_command_plan_state(&mut entry, plan, profile_launch);
286
+ agent_map.insert(
287
+ as_agent_id.as_str().to_string(),
288
+ serde_json::Value::Object(entry),
289
+ );
290
+ if let Some(entry) = agent_map
291
+ .get_mut(as_agent_id.as_str())
292
+ .and_then(serde_json::Value::as_object_mut)
293
+ {
294
+ persist_effective_approval_policy(entry, safety);
295
+ }
296
+ Ok(())
297
+ }
@@ -0,0 +1,372 @@
1
+ use std::collections::{BTreeMap, BTreeSet};
2
+ use std::path::{Path, PathBuf};
3
+ use std::process::Command;
4
+
5
+ use crate::lifecycle::*;
6
+ use crate::model::enums::{AuthMode, DisplayBackend, PaneLiveness, Provider, ProviderEffort};
7
+ use crate::model::ids::AgentId;
8
+ use crate::model::permissions::{self, AgentPermissionInput};
9
+ use crate::model::yaml::{self, Value};
10
+ use crate::state::persist::load_runtime_state;
11
+ use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
12
+
13
+ use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
14
+
15
+ use super::*;
16
+
17
+ pub(super) fn spec_team_id(spec: &Value) -> Option<String> {
18
+ spec.get("team")
19
+ .and_then(|v| v.get("id").or_else(|| v.get("name")))
20
+ .and_then(Value::as_str)
21
+ .map(str::to_string)
22
+ .or_else(|| spec.get("name").and_then(Value::as_str).map(str::to_string))
23
+ }
24
+
25
+ pub(super) fn explicit_active_team_key(state: &serde_json::Value) -> Option<String> {
26
+ state
27
+ .get("active_team_key")
28
+ .and_then(serde_json::Value::as_str)
29
+ .filter(|team| !team.is_empty())
30
+ .map(str::to_string)
31
+ }
32
+
33
+ pub(super) fn runtime_team_key_for_spec(
34
+ spec_path: &Path,
35
+ spec: &Value,
36
+ session_name: &SessionName,
37
+ ) -> String {
38
+ let team_dir = spec_path.parent().unwrap_or_else(|| Path::new("."));
39
+ let state = serde_json::json!({
40
+ "team_dir": team_dir.to_string_lossy(),
41
+ "spec_path": spec_path.to_string_lossy(),
42
+ "session_name": session_name.as_str(),
43
+ "team": spec.get("team").map(yaml_value_to_json).unwrap_or(serde_json::Value::Null),
44
+ });
45
+ crate::state::projection::team_state_key(&state)
46
+ }
47
+
48
+ pub(super) fn transport_has_session(transport: &dyn Transport, session_name: &SessionName) -> bool {
49
+ match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
50
+ transport.has_session(session_name)
51
+ })) {
52
+ Ok(Ok(live)) => live,
53
+ Ok(Err(_)) | Err(_) => false,
54
+ }
55
+ }
56
+
57
+ pub(super) fn spec_display_backend(spec: &Value) -> DisplayBackend {
58
+ let requested = spec
59
+ .get("runtime")
60
+ .and_then(|runtime| runtime.get("display_backend"))
61
+ .and_then(Value::as_str)
62
+ .and_then(|backend| {
63
+ serde_json::from_value::<DisplayBackend>(serde_json::json!(backend)).ok()
64
+ });
65
+ crate::lifecycle::display::resolve_display_backend(requested, None).backend
66
+ }
67
+
68
+ use crate::provider::wire::parse_provider;
69
+
70
+ pub(super) fn parse_auth_mode(raw: &str) -> Option<AuthMode> {
71
+ match raw {
72
+ "subscription" => Some(AuthMode::Subscription),
73
+ "official_api" => Some(AuthMode::OfficialApi),
74
+ "compatible_api" => Some(AuthMode::CompatibleApi),
75
+ _ => None,
76
+ }
77
+ }
78
+
79
+ /// 0.4.x provider effort MVP step 4: low-level from a raw string. Returns
80
+ /// `Some(effort)` when the level parses AND the provider supports it.
81
+ pub(crate) fn provider_effort_from_raw(
82
+ raw: Option<&str>,
83
+ provider: Provider,
84
+ ) -> Option<ProviderEffort> {
85
+ let raw = raw?.trim();
86
+ if raw.is_empty() {
87
+ return None;
88
+ }
89
+ let effort = ProviderEffort::parse(raw)?;
90
+ if effort.is_supported_by(provider) {
91
+ Some(effort)
92
+ } else {
93
+ None
94
+ }
95
+ }
96
+
97
+ /// 0.4.x provider effort MVP step 7: warning event payload when the spec
98
+ /// requested an effort level the provider does not support.
99
+ pub(crate) fn provider_effort_event_payload(
100
+ raw: Option<&str>,
101
+ provider: Provider,
102
+ agent_id: &str,
103
+ ) -> Option<serde_json::Value> {
104
+ let raw = raw?.trim();
105
+ if raw.is_empty() {
106
+ return None;
107
+ }
108
+ let effort = ProviderEffort::parse(raw)?;
109
+ if effort.is_supported_by(provider) {
110
+ return None;
111
+ }
112
+ Some(serde_json::json!({
113
+ "agent_id": agent_id,
114
+ "provider": format!("{provider:?}").to_lowercase(),
115
+ "effort": effort.as_str(),
116
+ "action": "ignored",
117
+ "reason": "provider does not support effort",
118
+ }))
119
+ }
120
+
121
+ /// 0.4.x provider effort MVP step 9: defensive guarantee that `CLAUDE_EFFORT`
122
+ /// is unset in the Claude/ClaudeCode worker spawn env. As of the
123
+ /// `profile_launch::provider_env_unsets` update, the base list already
124
+ /// includes `CLAUDE_EFFORT` for Claude — so this function is idempotent
125
+ /// (returns input unchanged). Kept as a belt-and-braces guard so a future
126
+ /// refactor that bypasses provider_env_unsets cannot silently drop the
127
+ /// scrub. The structural win is in `tmux_backend::shell_command` which now
128
+ /// filters env exports by env_unset (preventing inherited values from
129
+ /// re-introducing keys we just unset).
130
+ pub(crate) fn extend_worker_env_unset_for_effort(
131
+ base: Vec<String>,
132
+ provider: Provider,
133
+ ) -> Vec<String> {
134
+ if !matches!(provider, Provider::Claude | Provider::ClaudeCode) {
135
+ return base;
136
+ }
137
+ let mut out = base;
138
+ if !out.iter().any(|k| k == "CLAUDE_EFFORT") {
139
+ out.push("CLAUDE_EFFORT".to_string());
140
+ }
141
+ out
142
+ }
143
+
144
+ /// Convenience: resolve effort for a yaml::Value agent (spec / compiled).
145
+ pub(crate) fn provider_effort_for_spawn(
146
+ agent: &crate::model::yaml::Value,
147
+ provider: Provider,
148
+ ) -> Option<ProviderEffort> {
149
+ provider_effort_from_raw(agent.get("effort").and_then(|v| v.as_str()), provider)
150
+ }
151
+
152
+ pub(crate) fn provider_effort_event_if_dropped(
153
+ agent: &crate::model::yaml::Value,
154
+ provider: Provider,
155
+ agent_id: &str,
156
+ ) -> Option<serde_json::Value> {
157
+ provider_effort_event_payload(
158
+ agent.get("effort").and_then(|v| v.as_str()),
159
+ provider,
160
+ agent_id,
161
+ )
162
+ }
163
+
164
+ /// Same as [`provider_effort_for_spawn`] but for serde_json state values
165
+ /// (used by restart paths reading from `state.agents[id]`).
166
+ pub(crate) fn provider_effort_for_spawn_json(
167
+ agent: &serde_json::Value,
168
+ provider: Provider,
169
+ ) -> Option<ProviderEffort> {
170
+ provider_effort_from_raw(
171
+ agent.get("effort").and_then(serde_json::Value::as_str),
172
+ provider,
173
+ )
174
+ }
175
+
176
+ pub(crate) fn provider_effort_event_if_dropped_json(
177
+ agent: &serde_json::Value,
178
+ provider: Provider,
179
+ agent_id: &str,
180
+ ) -> Option<serde_json::Value> {
181
+ provider_effort_event_payload(
182
+ agent.get("effort").and_then(serde_json::Value::as_str),
183
+ provider,
184
+ agent_id,
185
+ )
186
+ }
187
+
188
+ pub(super) fn quick_start_requested_team_key<'a>(
189
+ team_id: Option<&'a str>,
190
+ name: Option<&'a str>,
191
+ ) -> Option<&'a str> {
192
+ team_id.or(name).filter(|team| !team.is_empty())
193
+ }
194
+
195
+ pub(super) struct QuickStartDepth {
196
+ pub(super) parent_team_key: Option<String>,
197
+ pub(super) team_depth: u64,
198
+ }
199
+
200
+ pub(super) fn quick_start_depth_guard(
201
+ workspace: &Path,
202
+ _agents_dir: &Path,
203
+ requested_team: Option<&str>,
204
+ _strict_real_runtime: bool,
205
+ ) -> Result<QuickStartDepth, LifecycleError> {
206
+ let env_parent = std::env::var("TEAM_AGENT_OWNER_TEAM_ID")
207
+ .ok()
208
+ .map(|value| value.trim().to_string())
209
+ .filter(|value| !value.is_empty());
210
+ let parent = env_parent;
211
+ let Some(parent) = parent else {
212
+ let state = crate::state::persist::load_runtime_state(workspace)
213
+ .unwrap_or_else(|_| serde_json::json!({}));
214
+ let ambiguous_nested_intent = requested_team.is_some_and(|team| {
215
+ looks_ambiguous_child_team_key(team) || looks_grandchild_team_key(team)
216
+ });
217
+ if has_live_runtime_teams(&state) && ambiguous_nested_intent {
218
+ if requested_team.is_some_and(looks_grandchild_team_key) {
219
+ if let Some(parent_key) = infer_parent_team_from_active_state(&state) {
220
+ let parent_state =
221
+ crate::state::projection::project_top_level_view(&state, &parent_key);
222
+ let parent_depth = parent_state
223
+ .get("team_depth")
224
+ .and_then(serde_json::Value::as_u64)
225
+ .unwrap_or(1);
226
+ return Ok(QuickStartDepth {
227
+ parent_team_key: Some(parent_key),
228
+ team_depth: parent_depth.saturating_add(1),
229
+ });
230
+ }
231
+ }
232
+ return Err(LifecycleError::RequirementUnmet(
233
+ "cannot infer parent team for nested quick-start; pass an explicit worker/subleader owner context"
234
+ .to_string(),
235
+ ));
236
+ }
237
+ return Ok(QuickStartDepth {
238
+ parent_team_key: None,
239
+ team_depth: 1,
240
+ });
241
+ };
242
+ let state = crate::state::persist::load_runtime_state(workspace)
243
+ .unwrap_or_else(|_| serde_json::json!({}));
244
+ let parent_key = crate::state::projection::resolve_owner_team_id(&state, &parent)
245
+ .canonical_key()
246
+ .map(str::to_string)
247
+ .unwrap_or(parent);
248
+ let parent_state = crate::state::projection::project_top_level_view(&state, &parent_key);
249
+ let parent_depth = parent_state
250
+ .get("team_depth")
251
+ .and_then(serde_json::Value::as_u64)
252
+ .unwrap_or(1);
253
+ let team_depth = parent_depth.saturating_add(1);
254
+ Ok(QuickStartDepth {
255
+ parent_team_key: Some(parent_key),
256
+ team_depth,
257
+ })
258
+ }
259
+
260
+ pub(super) fn infer_parent_team_from_active_state(state: &serde_json::Value) -> Option<String> {
261
+ let active = explicit_active_team_key(state)?;
262
+ let team = state
263
+ .get("teams")
264
+ .and_then(serde_json::Value::as_object)
265
+ .and_then(|teams| teams.get(&active))?;
266
+ team_has_running_agent(team).then_some(active)
267
+ }
268
+
269
+ pub(super) fn has_live_runtime_teams(state: &serde_json::Value) -> bool {
270
+ state
271
+ .get("teams")
272
+ .and_then(serde_json::Value::as_object)
273
+ .is_some_and(|teams| teams.values().any(team_has_running_agent))
274
+ }
275
+
276
+ pub(super) fn team_has_running_agent(team: &serde_json::Value) -> bool {
277
+ team.get("agents")
278
+ .and_then(serde_json::Value::as_object)
279
+ .is_some_and(|agents| {
280
+ agents.values().any(|agent| {
281
+ agent.get("status").and_then(serde_json::Value::as_str) == Some("running")
282
+ })
283
+ })
284
+ }
285
+
286
+ pub(super) fn looks_ambiguous_child_team_key(team: &str) -> bool {
287
+ let team = team.trim().to_ascii_lowercase();
288
+ team != "child"
289
+ && (team.starts_with("child-")
290
+ || team.starts_with("child_")
291
+ || team.starts_with("child.")
292
+ || team.starts_with("child"))
293
+ }
294
+
295
+ pub(super) fn looks_grandchild_team_key(team: &str) -> bool {
296
+ let team = team.trim().to_ascii_lowercase();
297
+ team == "grandchild"
298
+ || team.starts_with("grandchild-")
299
+ || team.starts_with("grandchild_")
300
+ || team.starts_with("grandchild.")
301
+ || team.starts_with("grandchild")
302
+ }
303
+
304
+ pub(super) fn annotate_team_depth(
305
+ state: &mut serde_json::Value,
306
+ parent_team_key: Option<&str>,
307
+ team_depth: u64,
308
+ ) {
309
+ let Some(obj) = state.as_object_mut() else {
310
+ return;
311
+ };
312
+ obj.insert("team_depth".to_string(), serde_json::json!(team_depth));
313
+ if let Some(parent) = parent_team_key.filter(|value| !value.is_empty()) {
314
+ obj.insert("parent_team_key".to_string(), serde_json::json!(parent));
315
+ }
316
+ }
317
+
318
+ pub(super) fn annotate_persisted_team_depth(
319
+ workspace: &Path,
320
+ team_key: &str,
321
+ parent_team_key: Option<&str>,
322
+ team_depth: u64,
323
+ ) -> Result<(), LifecycleError> {
324
+ let mut state = crate::state::persist::load_runtime_state(workspace)
325
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
326
+ let Some(team) = state
327
+ .get_mut("teams")
328
+ .and_then(serde_json::Value::as_object_mut)
329
+ .and_then(|teams| teams.get_mut(team_key))
330
+ else {
331
+ return Ok(());
332
+ };
333
+ annotate_team_depth(team, parent_team_key, team_depth);
334
+ crate::state::repository::StateRepository::new(workspace)
335
+ .save(
336
+ crate::state::repository::StateWriteIntent::AnnotateTeamDepth { team_key },
337
+ &state,
338
+ )
339
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
340
+ Ok(())
341
+ }
342
+
343
+ pub(super) fn runtime_state_has_quick_start_team(state: &serde_json::Value, team: &str) -> bool {
344
+ explicit_active_team_key(state).as_deref() == Some(team)
345
+ || state
346
+ .get("teams")
347
+ .and_then(serde_json::Value::as_object)
348
+ .is_some_and(|teams| {
349
+ teams.contains_key(team)
350
+ || teams
351
+ .values()
352
+ .any(|entry| json_team_identity_matches(entry, team))
353
+ })
354
+ || crate::state::projection::team_state_key(state) == team
355
+ || json_team_identity_matches(state, team)
356
+ || state
357
+ .get("session_name")
358
+ .and_then(serde_json::Value::as_str)
359
+ .is_some_and(|session| session == team || session.strip_prefix("team-") == Some(team))
360
+ }
361
+
362
+ pub(super) fn json_team_identity_matches(state: &serde_json::Value, team: &str) -> bool {
363
+ state
364
+ .get("team")
365
+ .and_then(|value| value.get("id").or_else(|| value.get("name")))
366
+ .and_then(serde_json::Value::as_str)
367
+ .is_some_and(|value| value == team)
368
+ || state
369
+ .get("name")
370
+ .and_then(serde_json::Value::as_str)
371
+ .is_some_and(|value| value == team)
372
+ }