@team-agent/installer 0.4.10 → 0.5.0

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 (74) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +1 -0
  4. package/crates/team-agent/src/cli/diagnose.rs +2 -15
  5. package/crates/team-agent/src/cli/emit.rs +62 -4
  6. package/crates/team-agent/src/compiler.rs +11 -20
  7. package/crates/team-agent/src/coordinator/orphan.rs +2 -2
  8. package/crates/team-agent/src/coordinator/runtime_detectors.rs +5 -4
  9. package/crates/team-agent/src/coordinator/steps/abnormal.rs +1776 -2
  10. package/crates/team-agent/src/coordinator/steps/mod.rs +19 -0
  11. package/crates/team-agent/src/coordinator/tests/a0_lostupdate.rs +78 -7
  12. package/crates/team-agent/src/coordinator/tests/abnormal.rs +195 -0
  13. package/crates/team-agent/src/coordinator/tick.rs +31 -932
  14. package/crates/team-agent/src/diagnose/orphans.rs +66 -8
  15. package/crates/team-agent/src/layout/worker_window_helpers.rs +5 -7
  16. package/crates/team-agent/src/leader/provider_attribution.rs +7 -5
  17. package/crates/team-agent/src/leader/rediscover.rs +1 -11
  18. package/crates/team-agent/src/leader/start.rs +169 -25
  19. package/crates/team-agent/src/lifecycle/launch/plan.rs +19 -8
  20. package/crates/team-agent/src/lifecycle/launch.rs +135 -58
  21. package/crates/team-agent/src/lifecycle/lock.rs +302 -0
  22. package/crates/team-agent/src/lifecycle/mod.rs +1 -0
  23. package/crates/team-agent/src/lifecycle/profile_smoke.rs +1 -11
  24. package/crates/team-agent/src/lifecycle/restart/agent.rs +436 -121
  25. package/crates/team-agent/src/lifecycle/restart/common.rs +108 -17
  26. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +165 -32
  27. package/crates/team-agent/src/lifecycle/restart/remove.rs +16 -2
  28. package/crates/team-agent/src/lifecycle/restart/selection.rs +2 -1
  29. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +193 -0
  30. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +3 -3
  31. package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +321 -0
  32. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +54 -15
  33. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +802 -0
  34. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +648 -0
  35. package/crates/team-agent/src/lifecycle/tests.rs +3 -0
  36. package/crates/team-agent/src/lifecycle/types.rs +8 -0
  37. package/crates/team-agent/src/lifecycle/worker_command_context.rs +24 -34
  38. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +24 -9
  39. package/crates/team-agent/src/mcp_server/tools.rs +157 -68
  40. package/crates/team-agent/src/messaging/activity.rs +43 -18
  41. package/crates/team-agent/src/messaging/delivery.rs +161 -97
  42. package/crates/team-agent/src/messaging/helpers.rs +1 -0
  43. package/crates/team-agent/src/messaging/leader_receiver.rs +22 -1
  44. package/crates/team-agent/src/messaging/results.rs +34 -9
  45. package/crates/team-agent/src/messaging/scheduler.rs +52 -9
  46. package/crates/team-agent/src/model/spec.rs +10 -6
  47. package/crates/team-agent/src/provider/adapter.rs +10 -1038
  48. package/crates/team-agent/src/provider/adapters/claude.rs +3 -8
  49. package/crates/team-agent/src/provider/adapters/copilot.rs +2 -5
  50. package/crates/team-agent/src/provider/classify.rs +23 -48
  51. package/crates/team-agent/src/provider/command.rs +16 -7
  52. package/crates/team-agent/src/provider/faults.rs +93 -53
  53. package/crates/team-agent/src/provider/session/capture.rs +4 -15
  54. package/crates/team-agent/src/provider/session_scan/claude.rs +309 -0
  55. package/crates/team-agent/src/provider/session_scan/codex.rs +40 -0
  56. package/crates/team-agent/src/provider/session_scan/common.rs +350 -0
  57. package/crates/team-agent/src/provider/session_scan/copilot.rs +202 -0
  58. package/crates/team-agent/src/provider/session_scan.rs +65 -27
  59. package/crates/team-agent/src/provider/tests/faults.rs +80 -0
  60. package/crates/team-agent/src/provider/types.rs +33 -1
  61. package/crates/team-agent/src/provider/wire.rs +171 -1
  62. package/crates/team-agent/src/state/identity.rs +242 -57
  63. package/crates/team-agent/src/state/identity_keys.rs +9 -12
  64. package/crates/team-agent/src/state/mod.rs +5 -1
  65. package/crates/team-agent/src/state/owner_gate.rs +59 -15
  66. package/crates/team-agent/src/state/ownership.rs +12 -11
  67. package/crates/team-agent/src/state/paths.rs +13 -6
  68. package/crates/team-agent/src/state/persist.rs +671 -128
  69. package/crates/team-agent/src/state/projection.rs +240 -49
  70. package/crates/team-agent/src/state/selector.rs +11 -5
  71. package/crates/team-agent/src/tmux_backend/tests.rs +51 -2
  72. package/crates/team-agent/src/tmux_backend.rs +42 -5
  73. package/crates/team-agent/src/transport/test_support.rs +55 -6
  74. package/package.json +4 -4
@@ -7,45 +7,35 @@ use crate::model::permissions::{resolve_permissions, AgentPermissionInput};
7
7
 
8
8
  const RUNTIME_CONTRACT_SECTION: &str = r#"# Team Agent Teammate Runtime Contract
9
9
 
10
- You are a teammate in a Team Agent runtime, not the user's primary assistant.
11
- The user normally talks to the team lead. Plain text you write in this worker
12
- session is local to this session and is not a team message.
10
+ You are a teammate in a Team Agent runtime. The leader cannot see your terminal
11
+ output. All communication must go through Team Agent MCP tools.
13
12
 
14
- Use Team Agent MCP tools for team-visible coordination:
15
- - Send progress, blockers, permission needs, tool failures, scope changes, and
16
- long-running status updates with team_orchestrator.send_message(to='leader',
17
- content='<short message>').
18
- - Send to another teammate by agent id when coordination is useful, or use
19
- to='*' to notify every other team member. The runtime resolves only this team
20
- and excludes your own worker.
21
- - When the task is complete, call team_orchestrator.report_result exactly once.
22
- - Do not pass sender, task_id, agent_id, schema_version, or ack fields unless
23
- doing a low-level compatibility diagnostic. The MCP runtime fills protocol
24
- fields from the current worker and task state.
25
- - Emergency fallback exception: if a leader-bound MCP send/report payload is
26
- already built but the MCP transport itself fails or the primary delivery path
27
- errors (for example `Transport closed`, `Connection refused`, `Broken pipe`,
28
- `EOF`, timeout, or internal delivery error), use `team-agent fallback-send-leader`
29
- or `team-agent fallback-report-result` exactly once with `--primary-error`.
30
- Do not use fallback for business refusals such as permission, quota, or unknown
31
- target. After fallback, tell the leader to run `team-agent restart-agent` to
32
- refresh the worker MCP transport.
13
+ ## Communication (mandatory)
33
14
 
34
- If you are blocked or cannot continue, message the leader promptly instead of
35
- waiting silently. If work takes several minutes, send a short progress update.
15
+ - Progress, blockers, questions: team_orchestrator.send_message(to='leader', content='...')
16
+ - Coordinate with teammate: team_orchestrator.send_message(to='<agent_id>', content='...')
17
+ - Broadcast to all teammates: team_orchestrator.send_message(to='*', content='...')
18
+ - Task complete: team_orchestrator.report_result(summary='...') — call exactly once
36
19
 
37
- When any Team Agent worker hits a 500/529/rate-limit/overloaded API error,
38
- slow the team down before retrying: wait 1-2 minutes, keep active workers low,
39
- and avoid blind immediate retries."#;
20
+ When you receive a message from the leader or a teammate, you MUST respond
21
+ through MCP tools. Writing a reply in your terminal does nothing the sender
22
+ will never see it.
40
23
 
24
+ ## Rules
25
+
26
+ - Do not pass sender, task_id, or schema_version — the MCP runtime fills them.
27
+ - If blocked or waiting, send_message to the leader. Do not wait silently.
28
+ - On 500/529/rate-limit errors, wait 1-2 minutes before retrying."#;
29
+
30
+ // 0.4.11 trimmed: the runtime contract section above already covers
31
+ // send_message signatures and report_result exactly-once. The output
32
+ // contract now only carries the RESULT-ENVELOPE-SPECIFIC delivery
33
+ // semantics (leader-attach dependence + fallback status) that the
34
+ // generic runtime section deliberately leaves out.
41
35
  const RESULT_ENVELOPE_OUTPUT_CONTRACT: &str =
42
- "For progress or blockers, call team_orchestrator.send_message(to='leader', content='<short message>'); \
43
- for teammate coordination, send to another agent id or to='*' for every other team member. \
44
- do not pass sender, task_id, or requires_ack because the MCP runtime fills protocol fields. \
45
- the runtime injects it into the attached Codex leader pane when the leader has run attach-leader. \
46
- If no leader is attached, the tool returns a fallback/failed result instead of completion. \
47
- Final completion must call team_orchestrator.report_result exactly once with a short summary \
48
- and optional status/changes/tests; MCP fills schema_version, task_id, and agent_id.";
36
+ "Final completion must call team_orchestrator.report_result exactly once with a short summary \
37
+ and optional status/changes/tests; the MCP runtime injects the result into the attached leader pane. \
38
+ If no leader is attached, the tool returns a fallback/failed result instead of completion.";
49
39
 
50
40
  pub(crate) struct WorkerCommandAgent {
51
41
  id: Option<String>,
@@ -26,13 +26,23 @@ pub(crate) fn update_state(
26
26
  let mut state = selected.state;
27
27
  ensure_object(&mut state);
28
28
  append_note(&mut state, note);
29
- crate::state::projection::save_team_scoped_state(&selected.run_workspace, &state)
30
- .map_err(tool_runtime_error)?;
29
+ crate::state::projection::save_team_scoped_state_reapplying_after_conflict(
30
+ &selected.run_workspace,
31
+ &state,
32
+ |latest| {
33
+ ensure_object(latest);
34
+ append_note(latest, note);
35
+ },
36
+ )
37
+ .map_err(tool_runtime_error)?;
31
38
  let spec_path = selected
32
39
  .spec_path
33
40
  .ok_or_else(|| tool_runtime_error("active team spec not found for update_state"))?;
34
41
  let spec_workspace = spec_path.parent().ok_or_else(|| {
35
- tool_runtime_error(format!("active team spec has no parent: {}", spec_path.display()))
42
+ tool_runtime_error(format!(
43
+ "active team spec has no parent: {}",
44
+ spec_path.display()
45
+ ))
36
46
  })?;
37
47
  let spec_text = std::fs::read_to_string(&spec_path).map_err(tool_runtime_error)?;
38
48
  let spec = crate::model::yaml::loads(&spec_text).map_err(tool_runtime_error)?;
@@ -56,8 +66,16 @@ fn update_state_without_spec(
56
66
  ensure_object(&mut state);
57
67
  seed_legacy_team_key(&mut state, &selected.run_workspace, &selected.team_key);
58
68
  append_note(&mut state, note);
59
- crate::state::projection::save_team_scoped_state(&selected.run_workspace, &state)
60
- .map_err(tool_runtime_error)?;
69
+ crate::state::projection::save_team_scoped_state_reapplying_after_conflict(
70
+ &selected.run_workspace,
71
+ &state,
72
+ |latest| {
73
+ ensure_object(latest);
74
+ seed_legacy_team_key(latest, &selected.run_workspace, &selected.team_key);
75
+ append_note(latest, note);
76
+ },
77
+ )
78
+ .map_err(tool_runtime_error)?;
61
79
  let path = crate::lifecycle::restart::write_team_state(
62
80
  &selected.run_workspace,
63
81
  &crate::model::yaml::Value::Null,
@@ -120,10 +138,7 @@ fn is_missing_active_spec(err: &crate::state::StateError) -> bool {
120
138
  )
121
139
  }
122
140
 
123
- pub(crate) fn get_team_status(
124
- workspace: &Path,
125
- owner_team: Option<&TeamKey>,
126
- ) -> ToolResult {
141
+ pub(crate) fn get_team_status(workspace: &Path, owner_team: Option<&TeamKey>) -> ToolResult {
127
142
  let selected = crate::state::selector::resolve_active_team(
128
143
  workspace,
129
144
  owner_team.map(TeamKey::as_str),
@@ -12,7 +12,9 @@ use crate::model::ids::{AgentId, TaskId, TeamKey};
12
12
  use crate::event_log::EventLog;
13
13
 
14
14
  // ── REUSE: step 5 state persist / projection ────────────────────────────────
15
- use crate::state::persist::{load_runtime_state, save_runtime_state};
15
+ use crate::state::persist::{
16
+ load_runtime_state, save_runtime_state, save_runtime_state_reapplying_after_conflict,
17
+ };
16
18
 
17
19
  // ── REUSE: step 11 messaging delegate surface ───────────────────────────────
18
20
  use crate::messaging::{self, MessageTarget, SendOptions};
@@ -20,13 +22,15 @@ use crate::messaging::{self, MessageTarget, SendOptions};
20
22
  use super::helpers::{
21
23
  delivery_outcome_value, ensure_object, enum_value, insert_array, is_worker_recipient,
22
24
  json_dumps_default, latest_task_for_assignee, latest_uncorrelated_delivered_message_for,
23
- non_empty_string, normalized_envelope_value, object_fields,
24
- requires_ack_for_target, tool_runtime_error,
25
+ non_empty_string, normalized_envelope_value, object_fields, requires_ack_for_target,
26
+ tool_runtime_error,
25
27
  };
26
28
  use super::normalize::{
27
29
  compact_tool_result, normalize_report_envelope, normalize_result_status_observed,
28
30
  };
29
- use super::types::{Scope, SendOutcome, ToolError, ToolErrorReason, ToolOk, ToolResult, VisiblePeers};
31
+ use super::types::{
32
+ Scope, SendOutcome, ToolError, ToolErrorReason, ToolOk, ToolResult, VisiblePeers,
33
+ };
30
34
 
31
35
  // ═══════════════════════════════════════════════════════════════════════════
32
36
  // TeamOrchestratorTools (tools.py:72) — the 12 typed tool handlers.
@@ -64,7 +68,11 @@ impl TeamOrchestratorTools {
64
68
 
65
69
  /// Test/explicit-injection constructor: bind identity/scope directly instead of
66
70
  /// reading env (so contracts can exercise scoped behavior deterministically).
67
- pub fn with_identity(workspace: &Path, agent_id: Option<AgentId>, owner_team_id: Option<TeamKey>) -> Self {
71
+ pub fn with_identity(
72
+ workspace: &Path,
73
+ agent_id: Option<AgentId>,
74
+ owner_team_id: Option<TeamKey>,
75
+ ) -> Self {
68
76
  Self {
69
77
  workspace: std::fs::canonicalize(workspace).unwrap_or_else(|_| workspace.to_path_buf()),
70
78
  agent_id,
@@ -85,7 +93,11 @@ impl TeamOrchestratorTools {
85
93
  "ValueError",
86
94
  ));
87
95
  };
88
- let Some(task_id) = task.get("id").and_then(Value::as_str).and_then(non_empty_string) else {
96
+ let Some(task_id) = task
97
+ .get("id")
98
+ .and_then(Value::as_str)
99
+ .and_then(non_empty_string)
100
+ else {
89
101
  return Err(ToolError::new(
90
102
  ToolErrorReason::InvalidToolArguments,
91
103
  "assign_task task.id is required",
@@ -112,7 +124,12 @@ impl TeamOrchestratorTools {
112
124
  .map(|team| team.as_str().to_string())
113
125
  .or_else(|| assignment_team_key(&state));
114
126
  reconcile_assigned_task(&mut state, team_key.as_deref(), &task_value);
115
- save_runtime_state(&self.workspace, &state).map_err(tool_runtime_error)?;
127
+ save_runtime_state_reapplying_after_conflict(&self.workspace, &state, |latest| {
128
+ ensure_object(latest);
129
+ let latest_team_key = team_key.clone().or_else(|| assignment_team_key(latest));
130
+ reconcile_assigned_task(latest, latest_team_key.as_deref(), &task_value);
131
+ })
132
+ .map_err(tool_runtime_error)?;
116
133
 
117
134
  let content = assignment_message(task, message);
118
135
  let out = self.send_message(
@@ -191,7 +208,8 @@ impl TeamOrchestratorTools {
191
208
  ..SendOptions::default()
192
209
  };
193
210
  if is_worker_recipient(to) {
194
- let out = messaging::send_message(&self.workspace, to, content, &opts).map_err(tool_runtime_error)?;
211
+ let out = messaging::send_message(&self.workspace, to, content, &opts)
212
+ .map_err(tool_runtime_error)?;
195
213
  // tools.py:175-181 — accepted+poll_via ONLY for a REAL message_id; any other
196
214
  // outcome falls back to the compacted direct result. Never invent an
197
215
  // `mcp_<timestamp>` id: it does not exist in the store and makes the
@@ -209,7 +227,8 @@ impl TeamOrchestratorTools {
209
227
  message_id,
210
228
  });
211
229
  }
212
- let out = messaging::send_message(&self.workspace, to, content, &opts).map_err(tool_runtime_error)?;
230
+ let out = messaging::send_message(&self.workspace, to, content, &opts)
231
+ .map_err(tool_runtime_error)?;
213
232
  let value = delivery_outcome_value(&out);
214
233
  let ok = compact_tool_result(&value)?;
215
234
  Ok(SendOutcome::Direct(ok))
@@ -219,7 +238,11 @@ impl TeamOrchestratorTools {
219
238
  self.rpc_scope_refused("unknown", None, None)
220
239
  }
221
240
 
222
- pub(crate) fn validate_rpc_scope_args(&self, tool: &str, args: &Value) -> Result<(), ToolError> {
241
+ pub(crate) fn validate_rpc_scope_args(
242
+ &self,
243
+ tool: &str,
244
+ args: &Value,
245
+ ) -> Result<(), ToolError> {
223
246
  if let Some(nested) = args.get("task").or_else(|| args.get("envelope")) {
224
247
  self.validate_rpc_scope_args(tool, nested)?;
225
248
  }
@@ -240,10 +263,11 @@ impl TeamOrchestratorTools {
240
263
  Ok(state) => state,
241
264
  Err(error) => return Err(self.scope_unverifiable(&error.to_string())),
242
265
  };
243
- let requested_canonical = crate::state::projection::resolve_owner_team_id(&state, requested)
244
- .canonical_key()
245
- .unwrap_or(requested)
246
- .to_string();
266
+ let requested_canonical =
267
+ crate::state::projection::resolve_owner_team_id(&state, requested)
268
+ .canonical_key()
269
+ .unwrap_or(requested)
270
+ .to_string();
247
271
  requested_canonical != owner.as_str()
248
272
  }
249
273
  (None, Some(_)) => true,
@@ -280,13 +304,17 @@ impl TeamOrchestratorTools {
280
304
  if let Some(envelope) = envelope {
281
305
  self.validate_rpc_scope_args("report_result", envelope)?;
282
306
  }
283
- let mut base = envelope.cloned().unwrap_or_else(|| Value::Object(serde_json::Map::new()));
307
+ let mut base = envelope
308
+ .cloned()
309
+ .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
284
310
  ensure_object(&mut base);
285
311
  if let Some(obj) = base.as_object_mut() {
286
312
  if !obj.contains_key("summary") {
287
313
  obj.insert(
288
314
  "summary".to_string(),
289
- Value::String(summary.map_or_else(|| "completed".to_string(), ToString::to_string)),
315
+ Value::String(
316
+ summary.map_or_else(|| "completed".to_string(), ToString::to_string),
317
+ ),
290
318
  );
291
319
  }
292
320
  if !obj.contains_key("status") {
@@ -302,33 +330,38 @@ impl TeamOrchestratorTools {
302
330
  // task id and no result yet (message-scope correlation —
303
331
  // collect path: is_message_scoped_result)
304
332
  // 4. "manual" — truly uncorrelated; collect still rejects
305
- let owner_team_id_str = self
306
- .owner_team_id
307
- .as_ref()
308
- .map(|t| t.as_str().to_string());
333
+ let owner_team_id_str = self.owner_team_id.as_ref().map(|t| t.as_str().to_string());
309
334
  let resolved = task_id
310
335
  .map(ToString::to_string)
311
- .or_else(|| self.agent_id.as_ref().and_then(|agent| {
312
- latest_task_for_assignee(
313
- &self.workspace,
314
- agent.as_str(),
315
- owner_team_id_str.as_deref(),
316
- )
317
- }))
318
- .or_else(|| self.agent_id.as_ref().and_then(|agent| {
319
- latest_uncorrelated_delivered_message_for(
320
- &self.workspace,
321
- agent.as_str(),
322
- owner_team_id_str.as_deref(),
323
- )
324
- }))
336
+ .or_else(|| {
337
+ self.agent_id.as_ref().and_then(|agent| {
338
+ latest_task_for_assignee(
339
+ &self.workspace,
340
+ agent.as_str(),
341
+ owner_team_id_str.as_deref(),
342
+ )
343
+ })
344
+ })
345
+ .or_else(|| {
346
+ self.agent_id.as_ref().and_then(|agent| {
347
+ latest_uncorrelated_delivered_message_for(
348
+ &self.workspace,
349
+ agent.as_str(),
350
+ owner_team_id_str.as_deref(),
351
+ )
352
+ })
353
+ })
325
354
  .unwrap_or_else(|| "manual".to_string());
326
355
  obj.insert("task_id".to_string(), Value::String(resolved));
327
356
  }
328
357
  if !obj.contains_key("agent_id") {
329
358
  let resolved = agent_id
330
359
  .map(ToString::to_string)
331
- .or_else(|| self.agent_id.as_ref().map(|env_agent| env_agent.as_str().to_string()))
360
+ .or_else(|| {
361
+ self.agent_id
362
+ .as_ref()
363
+ .map(|env_agent| env_agent.as_str().to_string())
364
+ })
332
365
  .unwrap_or_else(|| "unknown".to_string());
333
366
  obj.insert("agent_id".to_string(), Value::String(resolved));
334
367
  }
@@ -364,8 +397,8 @@ impl TeamOrchestratorTools {
364
397
  &env_value,
365
398
  owner_team.as_ref().map(TeamKey::as_str),
366
399
  )
367
- .map_err(tool_runtime_error)
368
- .and_then(|value| compact_tool_result(&value))
400
+ .map_err(tool_runtime_error)
401
+ .and_then(|value| compact_tool_result(&value))
369
402
  }
370
403
 
371
404
  /// T3-1 cr verdict: the observable record of an unknown→Partial status
@@ -405,7 +438,12 @@ impl TeamOrchestratorTools {
405
438
  /// `reset_agent` (`tools.py:333-334`): delegated through the lifecycle tools facade.
406
439
  pub fn reset_agent(&self, agent_id: &str, discard_session: bool) -> ToolResult {
407
440
  let owner_team = self.canonical_owner_team_key()?;
408
- super::lifecycle_tools::reset_agent(&self.workspace, owner_team.as_ref(), agent_id, discard_session)
441
+ super::lifecycle_tools::reset_agent(
442
+ &self.workspace,
443
+ owner_team.as_ref(),
444
+ agent_id,
445
+ discard_session,
446
+ )
409
447
  }
410
448
 
411
449
  /// `add_agent` (`tools.py:336-337`): delegate to real lifecycle add-agent
@@ -442,7 +480,12 @@ impl TeamOrchestratorTools {
442
480
  }
443
481
 
444
482
  /// `fork_agent` (`tools.py:339-340`): delegated through the lifecycle tools facade.
445
- pub fn fork_agent(&self, source_agent_id: &str, as_agent_id: &str, label: Option<&str>) -> ToolResult {
483
+ pub fn fork_agent(
484
+ &self,
485
+ source_agent_id: &str,
486
+ as_agent_id: &str,
487
+ label: Option<&str>,
488
+ ) -> ToolResult {
446
489
  let owner_team = self.canonical_owner_team_key()?;
447
490
  super::lifecycle_tools::fork_agent(
448
491
  &self.workspace,
@@ -456,7 +499,12 @@ impl TeamOrchestratorTools {
456
499
  /// `request_human` (`tools.py:342-346`): create a `requires_ack` leader message via
457
500
  /// the shared leader-delivery funnel; sender = env / inferred / `"unknown"`.
458
501
  /// Returns `{ok:true, message_id, status:"needs_human"}`.
459
- pub fn request_human(&self, question: &str, task_id: Option<&str>, agent_id: Option<&str>) -> ToolResult {
502
+ pub fn request_human(
503
+ &self,
504
+ question: &str,
505
+ task_id: Option<&str>,
506
+ agent_id: Option<&str>,
507
+ ) -> ToolResult {
460
508
  let _owner_team = self.canonical_owner_team_key()?;
461
509
  let explicit_sender = agent_id.and_then(non_empty_string);
462
510
  let sender = explicit_sender
@@ -495,9 +543,15 @@ impl TeamOrchestratorTools {
495
543
  fields.insert("ok".to_string(), Value::Bool(outcome.ok));
496
544
  fields.insert(
497
545
  "message_id".to_string(),
498
- outcome.message_id.clone().map_or(Value::Null, Value::String),
546
+ outcome
547
+ .message_id
548
+ .clone()
549
+ .map_or(Value::Null, Value::String),
550
+ );
551
+ fields.insert(
552
+ "status".to_string(),
553
+ Value::String("needs_human".to_string()),
499
554
  );
500
- fields.insert("status".to_string(), Value::String("needs_human".to_string()));
501
555
  Ok(ToolOk { fields })
502
556
  }
503
557
 
@@ -507,7 +561,9 @@ impl TeamOrchestratorTools {
507
561
  let _owner_team = self.canonical_owner_team_key()?;
508
562
  messaging::stuck_list(&self.workspace)
509
563
  .map_err(tool_runtime_error)
510
- .map(|v| ToolOk { fields: object_fields(v) })
564
+ .map(|v| ToolOk {
565
+ fields: object_fields(v),
566
+ })
511
567
  }
512
568
 
513
569
  /// `stuck_cancel` (`tools.py:351-352`): delegate to [`messaging::stuck_cancel`];
@@ -532,10 +588,16 @@ impl TeamOrchestratorTools {
532
588
  });
533
589
  }
534
590
  };
535
- let suppressed_by = self.agent_id.as_ref().map(AgentId::as_str).unwrap_or("leader");
591
+ let suppressed_by = self
592
+ .agent_id
593
+ .as_ref()
594
+ .map(AgentId::as_str)
595
+ .unwrap_or("leader");
536
596
  messaging::stuck_cancel(&self.workspace, agent_id, alert, suppressed_by)
537
597
  .map_err(tool_runtime_error)
538
- .map(|v| ToolOk { fields: object_fields(v) })
598
+ .map(|v| ToolOk {
599
+ fields: object_fields(v),
600
+ })
539
601
  }
540
602
 
541
603
  /// `get_visible_peers` (`tools.py:226-247`): C16 scope-filtered peer list — live
@@ -577,7 +639,11 @@ impl TeamOrchestratorTools {
577
639
  /// non-`*`/non-leader string target NOT in the visible-peer scope and not the
578
640
  /// sender itself, with `scope != workspace`, → `Some(ToolError{PeerNotInScope})`
579
641
  /// (also writes `mcp.send_message_refused`). `None` = allowed to proceed.
580
- pub fn refuse_cross_team_peer(&self, to: &MessageTarget, scope_override: Option<Scope>) -> Option<ToolError> {
642
+ pub fn refuse_cross_team_peer(
643
+ &self,
644
+ to: &MessageTarget,
645
+ scope_override: Option<Scope>,
646
+ ) -> Option<ToolError> {
581
647
  let owner_team = match self.canonical_owner_team_key() {
582
648
  Ok(team) => team,
583
649
  Err(error) => return Some(error),
@@ -599,7 +665,10 @@ impl TeamOrchestratorTools {
599
665
  || target == "*"
600
666
  || target == "leader"
601
667
  || target == "Leader"
602
- || self.agent_id.as_ref().is_some_and(|id| id.as_str() == target)
668
+ || self
669
+ .agent_id
670
+ .as_ref()
671
+ .is_some_and(|id| id.as_str() == target)
603
672
  {
604
673
  return None;
605
674
  }
@@ -620,10 +689,7 @@ impl TeamOrchestratorTools {
620
689
  );
621
690
  let mut extra = serde_json::Map::new();
622
691
  extra.insert("status".to_string(), Value::String("refused".to_string()));
623
- extra.insert(
624
- "hint".to_string(),
625
- Value::String(hint.to_string()),
626
- );
692
+ extra.insert("hint".to_string(), Value::String(hint.to_string()));
627
693
  Some(ToolError {
628
694
  reason: ToolErrorReason::PeerNotInScope,
629
695
  exc_type: "PeerNotInScope".to_string(),
@@ -649,7 +715,9 @@ impl TeamOrchestratorTools {
649
715
  .and_then(Value::as_object)
650
716
  .is_some_and(|teams| !teams.is_empty())
651
717
  {
652
- return Err(self.scope_refused("TEAM_AGENT_OWNER_TEAM_ID is required for multi-team MCP"));
718
+ return Err(
719
+ self.scope_refused("TEAM_AGENT_OWNER_TEAM_ID is required for multi-team MCP")
720
+ );
653
721
  }
654
722
  return Ok(None);
655
723
  };
@@ -674,7 +742,9 @@ impl TeamOrchestratorTools {
674
742
  .and_then(Value::as_object)
675
743
  .is_some_and(|teams| !teams.is_empty())
676
744
  {
677
- return Err(self.scope_refused("TEAM_AGENT_OWNER_TEAM_ID is required for multi-team MCP"));
745
+ return Err(
746
+ self.scope_refused("TEAM_AGENT_OWNER_TEAM_ID is required for multi-team MCP")
747
+ );
678
748
  }
679
749
  return Ok(None);
680
750
  };
@@ -701,7 +771,10 @@ impl TeamOrchestratorTools {
701
771
  );
702
772
  let mut extra = serde_json::Map::new();
703
773
  extra.insert("status".to_string(), Value::String("refused".to_string()));
704
- extra.insert("kind".to_string(), Value::String("scope_unverifiable".to_string()));
774
+ extra.insert(
775
+ "kind".to_string(),
776
+ Value::String("scope_unverifiable".to_string()),
777
+ );
705
778
  extra.insert(
706
779
  "next_action".to_string(),
707
780
  Value::String("retry shortly or check the runtime state path".to_string()),
@@ -744,7 +817,11 @@ impl TeamOrchestratorTools {
744
817
  requested_scope: Option<&str>,
745
818
  ) -> ToolError {
746
819
  let owner_team_id = self.canonical_owner_team_key_for_event();
747
- let agent_id = self.agent_id.as_ref().map(AgentId::as_str).unwrap_or("unknown");
820
+ let agent_id = self
821
+ .agent_id
822
+ .as_ref()
823
+ .map(AgentId::as_str)
824
+ .unwrap_or("unknown");
748
825
  let _ = EventLog::new(&self.workspace).write(
749
826
  "mcp.scope_refused",
750
827
  serde_json::json!({
@@ -794,10 +871,20 @@ fn canonicalize_owner_team_id(state: &Value, owner_team_id: &str) -> Option<Stri
794
871
  }
795
872
 
796
873
  fn requested_team_arg(args: &Value) -> Option<String> {
797
- ["team", "team_id", "owner_team_id", "owner_team", "target_team"]
798
- .iter()
799
- .find_map(|key| args.get(*key).and_then(Value::as_str).filter(|s| !s.is_empty()))
800
- .map(ToString::to_string)
874
+ [
875
+ "team",
876
+ "team_id",
877
+ "owner_team_id",
878
+ "owner_team",
879
+ "target_team",
880
+ ]
881
+ .iter()
882
+ .find_map(|key| {
883
+ args.get(*key)
884
+ .and_then(Value::as_str)
885
+ .filter(|s| !s.is_empty())
886
+ })
887
+ .map(ToString::to_string)
801
888
  }
802
889
 
803
890
  fn requested_scope_arg(args: &Value) -> Option<String> {
@@ -875,14 +962,12 @@ fn write_team_tasks(state: &mut Value, team_key: &str, tasks: Vec<Value>) {
875
962
  let Some(teams_obj) = teams.as_object_mut() else {
876
963
  return;
877
964
  };
878
- let team = teams_obj
879
- .entry(team_key.to_string())
880
- .or_insert_with(|| {
881
- let mut team = serde_json::Map::new();
882
- team.insert("tasks".to_string(), Value::Array(Vec::new()));
883
- team.insert("status".to_string(), Value::String("alive".to_string()));
884
- Value::Object(team)
885
- });
965
+ let team = teams_obj.entry(team_key.to_string()).or_insert_with(|| {
966
+ let mut team = serde_json::Map::new();
967
+ team.insert("tasks".to_string(), Value::Array(Vec::new()));
968
+ team.insert("status".to_string(), Value::String("alive".to_string()));
969
+ Value::Object(team)
970
+ });
886
971
  let Some(team_obj) = team.as_object_mut() else {
887
972
  return;
888
973
  };
@@ -894,7 +979,11 @@ fn assignment_message(task: &Value, explicit: Option<&str>) -> String {
894
979
  return message.to_string();
895
980
  }
896
981
  for key in ["description", "title"] {
897
- if let Some(text) = task.get(key).and_then(Value::as_str).and_then(non_empty_string) {
982
+ if let Some(text) = task
983
+ .get(key)
984
+ .and_then(Value::as_str)
985
+ .and_then(non_empty_string)
986
+ {
898
987
  return text.to_string();
899
988
  }
900
989
  }