@team-agent/installer 0.5.59 → 0.5.61

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 (68) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +44 -1
  4. package/crates/team-agent/src/cli/diagnose.rs +183 -0
  5. package/crates/team-agent/src/cli/emit.rs +63 -14
  6. package/crates/team-agent/src/cli/leader.rs +8 -1
  7. package/crates/team-agent/src/cli/mod.rs +263 -5
  8. package/crates/team-agent/src/cli/named_address.rs +18 -1
  9. package/crates/team-agent/src/cli/send/presentation.rs +2 -0
  10. package/crates/team-agent/src/cli/spec.rs +4 -1
  11. package/crates/team-agent/src/cli/status_port/store.rs +7 -3
  12. package/crates/team-agent/src/cli/tests/named_address.rs +65 -0
  13. package/crates/team-agent/src/cli/types.rs +33 -0
  14. package/crates/team-agent/src/communication_mode/mod.rs +53 -0
  15. package/crates/team-agent/src/compiler/tests.rs +5 -5
  16. package/crates/team-agent/src/compiler.rs +53 -1
  17. package/crates/team-agent/src/coordinator/tick.rs +21 -8
  18. package/crates/team-agent/src/db/message_store.rs +45 -1
  19. package/crates/team-agent/src/fake_worker.rs +21 -1
  20. package/crates/team-agent/src/leader/start.rs +732 -94
  21. package/crates/team-agent/src/leader/tests/identity.rs +64 -57
  22. package/crates/team-agent/src/leader/tests/identity_session_names.rs +42 -0
  23. package/crates/team-agent/src/lib.rs +4 -0
  24. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +58 -0
  25. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +57 -51
  26. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +148 -8
  27. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +5 -0
  28. package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
  29. package/crates/team-agent/src/lifecycle/launch.rs +1 -1
  30. package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -1
  31. package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
  32. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +35 -1
  33. package/crates/team-agent/src/lifecycle/types.rs +1 -0
  34. package/crates/team-agent/src/lifecycle/worker_command_context.rs +29 -11
  35. package/crates/team-agent/src/mcp_server/normalize.rs +1 -0
  36. package/crates/team-agent/src/mcp_server/tests/send.rs +26 -0
  37. package/crates/team-agent/src/mcp_server/tests/wire.rs +1 -1
  38. package/crates/team-agent/src/mcp_server/tools.rs +100 -66
  39. package/crates/team-agent/src/mcp_server/types.rs +5 -0
  40. package/crates/team-agent/src/mcp_server/wire.rs +27 -21
  41. package/crates/team-agent/src/messaging/delivery.rs +105 -38
  42. package/crates/team-agent/src/messaging/leader_channel.rs +33 -10
  43. package/crates/team-agent/src/messaging/leader_receiver.rs +40 -23
  44. package/crates/team-agent/src/messaging/presentation.rs +109 -0
  45. package/crates/team-agent/src/messaging/results.rs +165 -17
  46. package/crates/team-agent/src/messaging/send.rs +94 -81
  47. package/crates/team-agent/src/messaging/tests/leader_channel.rs +6 -6
  48. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +187 -0
  49. package/crates/team-agent/src/messaging/tests/runtime.rs +13 -6
  50. package/crates/team-agent/src/messaging/types.rs +5 -0
  51. package/crates/team-agent/src/messaging/watchers.rs +5 -0
  52. package/crates/team-agent/src/model/mod.rs +1 -0
  53. package/crates/team-agent/src/model/pane_authority_refusal.rs +358 -0
  54. package/crates/team-agent/src/model/spec.rs +16 -0
  55. package/crates/team-agent/src/provider/adapter.rs +57 -0
  56. package/crates/team-agent/src/provider/adapters/claude_fork.rs +10 -2
  57. package/crates/team-agent/src/provider/session/capture.rs +69 -34
  58. package/crates/team-agent/src/provider/session/context_fork/codex.rs +386 -9
  59. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +3 -14
  60. package/crates/team-agent/src/provider/session/context_fork.rs +7 -2
  61. package/crates/team-agent/src/provider/session/mod.rs +3 -2
  62. package/crates/team-agent/src/provider/session_scan/claude.rs +92 -3
  63. package/crates/team-agent/src/provider/session_scan/codex.rs +28 -0
  64. package/crates/team-agent/src/provider/session_scan/common.rs +34 -8
  65. package/crates/team-agent/src/provider/session_scan.rs +8 -0
  66. package/crates/team-agent/src/topology.rs +3 -0
  67. package/package.json +4 -4
  68. package/skills/team-agent/command-coverage.json +379 -0
@@ -120,7 +120,7 @@ pub(super) fn spawn_agents(
120
120
  agent,
121
121
  Some(agent_id_raw),
122
122
  provider,
123
- );
123
+ )?;
124
124
  let system_prompt =
125
125
  crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
126
126
  let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
@@ -295,8 +295,8 @@ mod fork_state;
295
295
  pub(super) use fork_state::*;
296
296
 
297
297
  mod fork_finalize;
298
- pub(crate) use fork_finalize::finalize_pending_fork_capture;
299
298
  pub(super) use fork_finalize::*;
299
+ pub(crate) use fork_finalize::{finalize_pending_fork_capture, ContextForkFinalized};
300
300
 
301
301
  mod role_source;
302
302
  pub(super) use role_source::*;
@@ -1635,7 +1635,7 @@ fn write_start_agent_start_event(
1635
1635
  agent,
1636
1636
  Some(agent_id.as_str()),
1637
1637
  provider,
1638
- );
1638
+ )?;
1639
1639
  let system_prompt =
1640
1640
  crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
1641
1641
  let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
@@ -220,7 +220,7 @@ pub(super) fn spawn_agent_window(
220
220
  agent,
221
221
  Some(agent_id.as_str()),
222
222
  provider,
223
- );
223
+ )?;
224
224
  let system_prompt =
225
225
  crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
226
226
  let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
@@ -1178,7 +1178,29 @@ pub(super) fn fork_ws(alpha_role: &str) -> PathBuf {
1178
1178
  std::process::id(),
1179
1179
  n
1180
1180
  ));
1181
- std::fs::write(&rollout, b"{}\n").expect("seed fork source rollout");
1181
+ std::fs::write(
1182
+ &rollout,
1183
+ format!(
1184
+ "{}\n{}\n",
1185
+ json!({
1186
+ "type": "session_meta",
1187
+ "payload": {
1188
+ "id": "sess-a",
1189
+ "cwd": ws,
1190
+ }
1191
+ }),
1192
+ json!({
1193
+ "type": "response_item",
1194
+ "payload": {
1195
+ "content": [{
1196
+ "type": "input_text",
1197
+ "text": "You are Team Agent worker `alpha` with role `fixture`."
1198
+ }]
1199
+ }
1200
+ })
1201
+ ),
1202
+ )
1203
+ .expect("seed identity-complete fork source rollout");
1182
1204
  crate::state::persist::save_runtime_state(
1183
1205
  &ws,
1184
1206
  &json!({
@@ -1669,6 +1691,18 @@ fn lanea_fork_gate_error_text_and_spec_rollback_on_adapter_arm() {
1669
1691
  fn lanea_fork_report_session_id_is_not_pane_id() {
1670
1692
  let _home = LaneHomeGuard::enter("fork-report");
1671
1693
  let ws = fork_ws(DELEG_ROLE_ALPHA); // codex+subscription -> native fork supported -> full success path
1694
+ let mut state = crate::state::persist::load_runtime_state(&ws).unwrap();
1695
+ let source = PathBuf::from(state["agents"]["alpha"]["rollout_path"].as_str().unwrap());
1696
+ let codex_root = std::env::var_os("HOME")
1697
+ .map(PathBuf::from)
1698
+ .unwrap()
1699
+ .join(".codex/sessions/lane-fixture");
1700
+ std::fs::create_dir_all(&codex_root).unwrap();
1701
+ let discoverable_source = codex_root.join("rollout-sess-a.jsonl");
1702
+ std::fs::copy(source, &discoverable_source).unwrap();
1703
+ state["agents"]["alpha"]["rollout_path"] =
1704
+ json!(discoverable_source.to_string_lossy().to_string());
1705
+ crate::state::persist::save_runtime_state(&ws, &state).unwrap();
1672
1706
  let tx = LaneTransport::new("team-laneateam", &[]);
1673
1707
  let report =
1674
1708
  fork_agent_with_transport(&ws, &aid("alpha"), &aid("newfork"), None, false, None, &tx)
@@ -36,6 +36,7 @@ pub mod event_names {
36
36
  pub const ADD_FAILED: &str = "lifecycle.add_failed";
37
37
  pub const REMOVE_STEP_COMPLETED: &str = "lifecycle.remove_step_completed";
38
38
  pub const REMOVE_ROLLED_BACK: &str = "lifecycle.remove_rolled_back";
39
+ pub const CONTEXT_FORK: &str = "context_fork";
39
40
  // restart 决策事件(Route B audit 契约必发)。
40
41
  pub const RESTART_RESUME_DECISION: &str = "restart.resume_decision";
41
42
  pub const RESTART_ATOMIC_REFUSAL: &str = "restart.atomic_refusal";
@@ -1,5 +1,6 @@
1
1
  use std::path::Path;
2
2
 
3
+ use crate::communication_mode::CommunicationMode;
3
4
  use crate::lifecycle::types::{DangerousApproval, LifecycleError};
4
5
  use crate::model::enums::{Enforcement, Provider};
5
6
  use crate::model::ids::AgentId;
@@ -12,15 +13,10 @@ output. All communication must go through Team Agent MCP tools.
12
13
 
13
14
  ## Communication (mandatory)
14
15
 
15
- - Progress, blockers, questions: team_orchestrator.send_message(to='leader', content='...')
16
16
  - Coordinate with teammate: team_orchestrator.send_message(to='<agent_id>', content='...')
17
17
  - Broadcast to all teammates: team_orchestrator.send_message(to='*', content='...')
18
18
  - Task complete: team_orchestrator.report_result(summary='...') — call exactly once
19
19
 
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.
23
-
24
20
  ## Rules
25
21
 
26
22
  - Do not pass sender, task_id, or schema_version — the MCP runtime fills them.
@@ -45,6 +41,7 @@ pub(crate) struct WorkerCommandAgent {
45
41
  system_prompt_inline: Option<String>,
46
42
  system_prompt_file: Option<String>,
47
43
  output_contract_format: Option<String>,
44
+ communication_mode: CommunicationMode,
48
45
  }
49
46
 
50
47
  impl WorkerCommandAgent {
@@ -52,9 +49,9 @@ impl WorkerCommandAgent {
52
49
  agent: &crate::model::yaml::Value,
53
50
  fallback_id: Option<&str>,
54
51
  provider: Provider,
55
- ) -> Self {
52
+ ) -> Result<Self, LifecycleError> {
56
53
  let system_prompt = agent.get("system_prompt");
57
- Self {
54
+ Ok(Self {
58
55
  id: agent
59
56
  .get("id")
60
57
  .and_then(crate::model::yaml::Value::as_str)
@@ -90,16 +87,21 @@ impl WorkerCommandAgent {
90
87
  .and_then(|contract| contract.get("format"))
91
88
  .and_then(crate::model::yaml::Value::as_str)
92
89
  .map(str::to_string),
93
- }
90
+ communication_mode: communication_mode(
91
+ agent
92
+ .get("communication_mode")
93
+ .and_then(crate::model::yaml::Value::as_str),
94
+ )?,
95
+ })
94
96
  }
95
97
 
96
98
  pub(crate) fn from_json(
97
99
  agent: &serde_json::Value,
98
100
  fallback_id: Option<&str>,
99
101
  provider: Provider,
100
- ) -> Self {
102
+ ) -> Result<Self, LifecycleError> {
101
103
  let system_prompt = agent.get("system_prompt");
102
- Self {
104
+ Ok(Self {
103
105
  id: agent
104
106
  .get("id")
105
107
  .and_then(serde_json::Value::as_str)
@@ -135,7 +137,12 @@ impl WorkerCommandAgent {
135
137
  .and_then(|contract| contract.get("format"))
136
138
  .and_then(serde_json::Value::as_str)
137
139
  .map(str::to_string),
138
- }
140
+ communication_mode: communication_mode(
141
+ agent
142
+ .get("communication_mode")
143
+ .and_then(serde_json::Value::as_str),
144
+ )?,
145
+ })
139
146
  }
140
147
  }
141
148
 
@@ -149,6 +156,7 @@ pub(crate) fn compile_worker_system_prompt(
149
156
  let mut chunks = vec![
150
157
  identity_section(agent),
151
158
  runtime_contract_section(),
159
+ agent.communication_mode.runtime_contract().to_string(),
152
160
  role_body(agent)?,
153
161
  ];
154
162
  if let Some(contract) = output_contract(agent) {
@@ -197,6 +205,14 @@ fn runtime_contract_section() -> String {
197
205
  RUNTIME_CONTRACT_SECTION.to_string()
198
206
  }
199
207
 
208
+ fn communication_mode(value: Option<&str>) -> Result<CommunicationMode, LifecycleError> {
209
+ let Some(value) = value else {
210
+ return Ok(CommunicationMode::default());
211
+ };
212
+ CommunicationMode::parse(value)
213
+ .ok_or_else(|| LifecycleError::Compile(format!("unknown communication_mode {value:?}")))
214
+ }
215
+
200
216
  fn identity_section(agent: &WorkerCommandAgent) -> String {
201
217
  format!(
202
218
  "You are Team Agent worker `{}` with role `{}`. When asked about your role or identity, answer with this Team Agent worker identity first, not only the generic provider product identity.",
@@ -291,6 +307,7 @@ mod tests {
291
307
  system_prompt_inline: None,
292
308
  system_prompt_file: None,
293
309
  output_contract_format: None,
310
+ communication_mode: CommunicationMode::default(),
294
311
  };
295
312
  let tools =
296
313
  resolved_tool_strings_for_command(&agent, Provider::ClaudeCode, &disabled_safety())
@@ -335,6 +352,7 @@ mod tests {
335
352
  system_prompt_inline: Some("Implement the assigned slice.".to_string()),
336
353
  system_prompt_file: None,
337
354
  output_contract_format: Some("result_envelope_v1".to_string()),
355
+ communication_mode: CommunicationMode::default(),
338
356
  };
339
357
  let prompt = compile_worker_system_prompt(&agent).unwrap();
340
358
  assert!(
@@ -260,6 +260,7 @@ pub fn compact_tool_result(result: &Value) -> ToolResult {
260
260
  "notification_channel",
261
261
  "notification_event_id",
262
262
  "warnings",
263
+ "warning",
263
264
  ]
264
265
  };
265
266
  for key in keys {
@@ -78,6 +78,7 @@ fn send_outcome_worker_accepted_envelope_byte_stable() {
78
78
  let outcome = SendOutcome::WorkerAccepted {
79
79
  message_id: "42".to_string(),
80
80
  poll_via: "team-agent inbox 42".to_string(),
81
+ warning: None,
81
82
  };
82
83
  let v = outcome.to_value();
83
84
  assert_eq!(
@@ -160,6 +161,7 @@ fn send_message_worker_recipient_returns_accepted_with_poll_hint() {
160
161
  Ok(SendOutcome::WorkerAccepted {
161
162
  message_id,
162
163
  poll_via,
164
+ ..
163
165
  }) => {
164
166
  assert!(!message_id.is_empty());
165
167
  assert_eq!(poll_via, format!("team-agent inbox {message_id}"));
@@ -353,6 +355,30 @@ fn send_message_leader_recipient_is_direct_not_accepted() {
353
355
  );
354
356
  }
355
357
 
358
+ #[test]
359
+ fn send_message_mailbox_is_durable_without_live_injection() {
360
+ let tools = TeamOrchestratorTools::with_identity(
361
+ &unique_ws("send-leader-mailbox"),
362
+ Some(AgentId::new("worker-1")),
363
+ Some(TeamKey::new("teamA")),
364
+ );
365
+ let mailbox = json!(true);
366
+ let outcome = tools
367
+ .send_message_with_presentation(
368
+ &MessageTarget::Single("leader".to_string()),
369
+ "stored update",
370
+ None,
371
+ None,
372
+ None,
373
+ Some(&mailbox),
374
+ None,
375
+ )
376
+ .expect("mailbox send persists");
377
+ let value = outcome.to_value();
378
+ assert_eq!(value.get("status"), Some(&json!("stored_only")));
379
+ assert!(value.get("message_id").and_then(Value::as_str).is_some());
380
+ }
381
+
356
382
  // ════════════════════════════════════════════════════════════════════════
357
383
  // CROSS-TEAM PRE-REFUSAL (C23) — refuse_cross_team_peer (tools.py:185-213)
358
384
  // ════════════════════════════════════════════════════════════════════════
@@ -79,7 +79,7 @@ fn tools_contract_has_thirteen_tools_in_order() {
79
79
  .unwrap();
80
80
  assert_eq!(
81
81
  send["description"],
82
- json!("Send a message to a teammate, the leader, or '*' for all other team members. Team Agent fills identity and delivery metadata; optional presentation routing is durable and never drops the message.")
82
+ json!("Send a message to a teammate, the leader, or '*' for all other team members. mailbox=true stores durably without live injection; the default is live delivery.")
83
83
  );
84
84
  assert_eq!(send["inputSchema"]["additionalProperties"], json!(false));
85
85
  assert_eq!(send["inputSchema"]["required"], json!(["to", "content"]));
@@ -17,17 +17,17 @@ use crate::state::persist::{
17
17
  };
18
18
 
19
19
  // ── REUSE: step 11 messaging delegate surface ───────────────────────────────
20
- use crate::messaging::{self, MessageTarget, SendOptions, TrustedSender};
20
+ use crate::messaging::{self, DeliveryStatus, MessageTarget, SendOptions, TrustedSender};
21
21
 
22
22
  use super::helpers::{
23
23
  current_reportable_message_for, delivery_outcome_value, direct_message_attribution_for,
24
- ensure_object, enum_value, insert_array, is_worker_recipient, json_dumps_default,
25
- latest_task_for_assignee, non_empty_string, normalized_envelope_value, object_fields,
26
- requires_ack_for_target, tool_runtime_error, DirectMessageAttribution,
24
+ ensure_object, enum_value, is_worker_recipient, json_dumps_default, latest_task_for_assignee,
25
+ non_empty_string, object_fields, requires_ack_for_target, tool_runtime_error,
26
+ DirectMessageAttribution,
27
27
  };
28
28
  use super::normalize::{
29
- compact_tool_result, normalize_report_envelope, normalize_result_status_observed,
30
- report_result_integrity_warnings, validate_test_evidence_schema,
29
+ compact_tool_result, normalize_report_envelope, report_result_integrity_warnings,
30
+ validate_test_evidence_schema,
31
31
  };
32
32
  use super::types::{
33
33
  Scope, SendOutcome, ToolError, ToolErrorReason, ToolOk, ToolResult, VisiblePeers,
@@ -116,6 +116,22 @@ impl TeamOrchestratorTools {
116
116
  "ValueError",
117
117
  ));
118
118
  };
119
+ if let Some(route) = task.get("result_route") {
120
+ let Some(route) = route.as_str() else {
121
+ return Err(ToolError::new(
122
+ ToolErrorReason::InvalidToolArguments,
123
+ "assign_task task.result_route must be 'leader' or 'pipeline'",
124
+ "ValueError",
125
+ ));
126
+ };
127
+ if crate::messaging::results::ResultRoute::parse(route).is_none() {
128
+ return Err(ToolError::new(
129
+ ToolErrorReason::InvalidToolArguments,
130
+ format!("assign_task task.result_route has unknown value: {route}"),
131
+ "ValueError",
132
+ ));
133
+ }
134
+ }
119
135
 
120
136
  let task_value = Value::Object(task_obj.clone());
121
137
  let recovery = task_recovery_marker(&task_value);
@@ -185,6 +201,7 @@ impl TeamOrchestratorTools {
185
201
  requires_ack,
186
202
  scope_override,
187
203
  None,
204
+ None,
188
205
  )
189
206
  }
190
207
 
@@ -195,17 +212,20 @@ impl TeamOrchestratorTools {
195
212
  task_id: Option<&str>,
196
213
  requires_ack: Option<bool>,
197
214
  scope_override: Option<Scope>,
215
+ mailbox: Option<&Value>,
198
216
  presentation: Option<&Value>,
199
217
  ) -> Result<SendOutcome, ToolError> {
200
- let (presentation, presentation_error) =
201
- crate::messaging::presentation::normalize_presentation(presentation);
202
- if let Some(error) = presentation_error {
203
- return Err(ToolError::new(
204
- ToolErrorReason::InvalidToolArguments,
205
- format!("invalid presentation: {error}"),
206
- "PresentationError",
207
- ));
208
- }
218
+ let normalized =
219
+ crate::messaging::presentation::normalize_send_presentation(mailbox, presentation)
220
+ .map_err(|error| {
221
+ ToolError::new(
222
+ ToolErrorReason::InvalidToolArguments,
223
+ format!("invalid send routing: {error}"),
224
+ "PresentationError",
225
+ )
226
+ })?;
227
+ let presentation = normalized.request;
228
+ let deprecation = normalized.deprecation;
209
229
  let canonical_owner_team = self.canonical_owner_team_key()?;
210
230
  if matches!(scope_override, Some(Scope::Workspace)) {
211
231
  return Err(self.rpc_scope_refused(
@@ -261,6 +281,21 @@ impl TeamOrchestratorTools {
261
281
  if is_worker_recipient(to) {
262
282
  let out = messaging::send_message(&self.workspace, to, content, &opts)
263
283
  .map_err(tool_runtime_error)?;
284
+ if matches!(out.status, DeliveryStatus::StoredOnly) {
285
+ let value = delivery_outcome_value(&out);
286
+ let mut ok = compact_tool_result(&value)?;
287
+ if let Some(verification) = out.verification.as_deref() {
288
+ ok.fields.insert(
289
+ "verification".to_string(),
290
+ Value::String(verification.to_string()),
291
+ );
292
+ }
293
+ if let Some(deprecation) = deprecation {
294
+ ok.fields
295
+ .insert("warning".to_string(), Value::String(deprecation));
296
+ }
297
+ return Ok(SendOutcome::Direct(ok));
298
+ }
264
299
  // tools.py:175-181 — accepted+poll_via ONLY for a REAL message_id; any other
265
300
  // outcome falls back to the compacted direct result. Never invent an
266
301
  // `mcp_<timestamp>` id: it does not exist in the store and makes the
@@ -276,12 +311,25 @@ impl TeamOrchestratorTools {
276
311
  return Ok(SendOutcome::WorkerAccepted {
277
312
  poll_via: format!("team-agent inbox {message_id}"),
278
313
  message_id,
314
+ warning: deprecation,
279
315
  });
280
316
  }
281
317
  let out = messaging::send_message(&self.workspace, to, content, &opts)
282
318
  .map_err(tool_runtime_error)?;
283
319
  let value = delivery_outcome_value(&out);
284
- let ok = compact_tool_result(&value)?;
320
+ let mut ok = compact_tool_result(&value)?;
321
+ if matches!(out.status, DeliveryStatus::StoredOnly) {
322
+ if let Some(verification) = out.verification.as_deref() {
323
+ ok.fields.insert(
324
+ "verification".to_string(),
325
+ Value::String(verification.to_string()),
326
+ );
327
+ }
328
+ }
329
+ if let Some(deprecation) = deprecation {
330
+ ok.fields
331
+ .insert("warning".to_string(), Value::String(deprecation));
332
+ }
285
333
  Ok(SendOutcome::Direct(ok))
286
334
  }
287
335
 
@@ -352,10 +400,11 @@ impl TeamOrchestratorTools {
352
400
  task_id: Option<&str>,
353
401
  agent_id: Option<&str>,
354
402
  ) -> ToolResult {
403
+ let status = enum_value(status);
355
404
  self.report_result_with_presentation(
356
405
  envelope,
357
406
  summary,
358
- status,
407
+ status.as_str(),
359
408
  changes,
360
409
  tests,
361
410
  risks,
@@ -372,7 +421,7 @@ impl TeamOrchestratorTools {
372
421
  &self,
373
422
  envelope: Option<&Value>,
374
423
  summary: Option<&str>,
375
- status: ResultStatus,
424
+ status: Option<&str>,
376
425
  changes: Option<&[Value]>,
377
426
  tests: Option<&[Value]>,
378
427
  risks: Option<&[Value]>,
@@ -404,7 +453,10 @@ impl TeamOrchestratorTools {
404
453
  );
405
454
  }
406
455
  if !obj.contains_key("status") {
407
- obj.insert("status".to_string(), enum_value(status));
456
+ obj.insert(
457
+ "status".to_string(),
458
+ Value::String(status.unwrap_or("success").to_string()),
459
+ );
408
460
  }
409
461
  if !obj.contains_key("task_id") {
410
462
  // Blocker-1 (prerelease 0.4.0): scoped-team task inference +
@@ -499,29 +551,36 @@ impl TeamOrchestratorTools {
499
551
  obj.insert("agent_id".to_string(), Value::String(resolved));
500
552
  }
501
553
  if !obj.contains_key("changes") {
502
- insert_array(obj, "changes", changes);
554
+ obj.insert(
555
+ "changes".to_string(),
556
+ Value::Array(changes.unwrap_or(&[]).to_vec()),
557
+ );
503
558
  }
504
559
  if !obj.contains_key("tests") {
505
- insert_array(obj, "tests", tests);
560
+ obj.insert(
561
+ "tests".to_string(),
562
+ Value::Array(tests.unwrap_or(&[]).to_vec()),
563
+ );
506
564
  }
507
565
  if !obj.contains_key("risks") {
508
- insert_array(obj, "risks", risks);
566
+ obj.insert(
567
+ "risks".to_string(),
568
+ Value::Array(risks.unwrap_or(&[]).to_vec()),
569
+ );
509
570
  }
510
571
  if !obj.contains_key("artifacts") {
511
- insert_array(obj, "artifacts", artifacts);
572
+ obj.insert(
573
+ "artifacts".to_string(),
574
+ Value::Array(artifacts.unwrap_or(&[]).to_vec()),
575
+ );
512
576
  }
513
577
  if !obj.contains_key("next_actions") {
514
- insert_array(obj, "next_actions", next_actions);
578
+ obj.insert(
579
+ "next_actions".to_string(),
580
+ Value::Array(next_actions.unwrap_or(&[]).to_vec()),
581
+ );
515
582
  }
516
583
  }
517
- // T3-1 cr verdict (refined): an unknown non-empty status literal normalizes to
518
- // Partial and must be OBSERVABLE at this ingestion boundary (the envelope-borne
519
- // path; the wire `status` arg path emits at dispatch). Never a silent swallow.
520
- if let Some(raw) =
521
- normalize_result_status_observed(base.get("status").and_then(Value::as_str)).1
522
- {
523
- self.note_unknown_result_status(&raw);
524
- }
525
584
  if let Err(error) = validate_test_evidence_schema(base.get("tests")) {
526
585
  return Err(ToolError::new(
527
586
  ToolErrorReason::InvalidToolArguments,
@@ -538,8 +597,15 @@ impl TeamOrchestratorTools {
538
597
  ));
539
598
  }
540
599
  let warnings = report_result_integrity_warnings(&base, &normalized);
541
- let mut env_value = normalized_envelope_value(&normalized);
542
- copy_report_attribution_fields(&base, &mut env_value);
600
+ let mut env_value = base;
601
+ if let Some(obj) = env_value.as_object_mut() {
602
+ obj.entry("schema_version")
603
+ .or_insert_with(|| Value::String("result_envelope_v1".to_string()));
604
+ obj.insert(
605
+ "presentation".to_string(),
606
+ serde_json::to_value(&normalized.presentation).unwrap_or(Value::Null),
607
+ );
608
+ }
543
609
  if !warnings.is_empty() {
544
610
  if let Some(obj) = env_value.as_object_mut() {
545
611
  obj.insert("warnings".to_string(), Value::Array(warnings));
@@ -555,20 +621,6 @@ impl TeamOrchestratorTools {
555
621
  .and_then(|value| compact_tool_result(&value))
556
622
  }
557
623
 
558
- /// T3-1 cr verdict: the observable record of an unknown→Partial status
559
- /// normalization (`provider.result.unknown_status_normalized`, raw literal
560
- /// included) — MUST-NOT-13: the swallow is never silent.
561
- pub(crate) fn note_unknown_result_status(&self, raw: &str) {
562
- let _ = EventLog::new(&self.workspace).write(
563
- "provider.result.unknown_status_normalized",
564
- serde_json::json!({
565
- "agent_id": self.agent_id.as_ref().map(AgentId::as_str),
566
- "raw_status": raw,
567
- "normalized": "partial",
568
- }),
569
- );
570
- }
571
-
572
624
  /// `update_state` (`tools.py:316-325`): delegated through the lifecycle tools
573
625
  /// facade. S0 preserves the old placeholder behavior.
574
626
  pub fn update_state(&self, note: &str) -> ToolResult {
@@ -1179,24 +1231,6 @@ fn merge_object_fields(existing: &mut Value, incoming: &Value) {
1179
1231
  }
1180
1232
  }
1181
1233
 
1182
- fn copy_report_attribution_fields(source: &Value, target: &mut Value) {
1183
- let Some(src) = source.as_object() else {
1184
- return;
1185
- };
1186
- let Some(dst) = target.as_object_mut() else {
1187
- return;
1188
- };
1189
- for key in [
1190
- "attributed_message_id",
1191
- "attribution_scope",
1192
- "task_id_source",
1193
- ] {
1194
- if let Some(value) = src.get(key) {
1195
- dst.entry(key.to_string()).or_insert(value.clone());
1196
- }
1197
- }
1198
- }
1199
-
1200
1234
  fn push_report_warning(obj: &mut serde_json::Map<String, Value>, warning: Value) {
1201
1235
  let Some(code) = warning.get("code").and_then(Value::as_str) else {
1202
1236
  return;
@@ -322,6 +322,7 @@ pub enum SendOutcome {
322
322
  message_id: String,
323
323
  /// Byte-stable `"team-agent inbox <message_id>"`.
324
324
  poll_via: String,
325
+ warning: Option<String>,
325
326
  },
326
327
  /// Compacted delegate result (leader / `*` / broadcast / fanout).
327
328
  Direct(ToolOk),
@@ -335,12 +336,16 @@ impl SendOutcome {
335
336
  SendOutcome::WorkerAccepted {
336
337
  message_id,
337
338
  poll_via,
339
+ warning,
338
340
  } => {
339
341
  let mut obj = serde_json::Map::new();
340
342
  obj.insert("status".to_string(), Value::String("accepted".to_string()));
341
343
  obj.insert("delivery_pending".to_string(), Value::Bool(true));
342
344
  obj.insert("poll_via".to_string(), Value::String(poll_via.clone()));
343
345
  obj.insert("message_id".to_string(), Value::String(message_id.clone()));
346
+ if let Some(warning) = warning {
347
+ obj.insert("warning".to_string(), Value::String(warning.clone()));
348
+ }
344
349
  Value::Object(obj)
345
350
  }
346
351
  SendOutcome::Direct(ok) => Value::Object(ok.fields.clone()),