@team-agent/installer 0.5.101 → 0.5.103

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 (49) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/emit.rs +2 -1
  4. package/crates/team-agent/src/cli/models.rs +92 -226
  5. package/crates/team-agent/src/cli/spec.rs +1 -1
  6. package/crates/team-agent/src/cli/tests/main_preserved.rs +0 -74
  7. package/crates/team-agent/src/compiler/tests.rs +20 -23
  8. package/crates/team-agent/src/compiler.rs +49 -65
  9. package/crates/team-agent/src/coordinator/tests/s3_bounded.rs +16 -48
  10. package/crates/team-agent/src/leader/start.rs +1 -14
  11. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +37 -161
  12. package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
  13. package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
  14. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +0 -28
  15. package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +11 -9
  16. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +8 -401
  17. package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +2 -2
  18. package/crates/team-agent/src/lifecycle/worker_command_context.rs +22 -8
  19. package/crates/team-agent/src/mcp_server/helpers.rs +0 -16
  20. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +2 -7
  21. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -127
  22. package/crates/team-agent/src/mcp_server/mod.rs +7 -12
  23. package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -132
  24. package/crates/team-agent/src/mcp_server/tests/send.rs +1 -93
  25. package/crates/team-agent/src/mcp_server/tests/tools.rs +0 -61
  26. package/crates/team-agent/src/mcp_server/tests/wire.rs +6 -51
  27. package/crates/team-agent/src/mcp_server/tests.rs +4 -200
  28. package/crates/team-agent/src/mcp_server/tools.rs +5 -396
  29. package/crates/team-agent/src/mcp_server/types.rs +3 -36
  30. package/crates/team-agent/src/mcp_server/wire.rs +2 -223
  31. package/crates/team-agent/src/messaging/leader_receiver.rs +1 -1
  32. package/crates/team-agent/src/messaging/mod.rs +2 -2
  33. package/crates/team-agent/src/messaging/results.rs +11 -42
  34. package/crates/team-agent/src/messaging/scheduler.rs +2 -205
  35. package/crates/team-agent/src/messaging/tests/basic.rs +0 -31
  36. package/crates/team-agent/src/messaging/tests/main_preserved.rs +0 -73
  37. package/crates/team-agent/src/messaging/tests/runtime.rs +0 -26
  38. package/crates/team-agent/src/messaging/types.rs +0 -20
  39. package/crates/team-agent/src/model/spec.rs +14 -18
  40. package/crates/team-agent/src/provider/mod.rs +1 -0
  41. package/crates/team-agent/src/provider/model_catalog.rs +650 -0
  42. package/crates/team-agent/src/state/repository/tests.rs +1 -16
  43. package/crates/team-agent/src/state/repository.rs +1 -33
  44. package/package.json +4 -4
  45. package/skills/team-agent/SKILL.md +5 -5
  46. package/skills/team-agent/command-coverage.json +7 -0
  47. package/skills/team-agent/references/team-agent-operator.md +24 -116
  48. package/skills/team-agent/references/team-in-team.md +4 -19
  49. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +0 -484
@@ -1,25 +1,8 @@
1
- //! ---
2
- //! purpose: MCP no-compaction contract test with fixture-owned workspace and path provenance
3
- //! contract:
4
- //! provides:
5
- //! - name: mcp_state_fixture
6
- //! what: supplies explicit process-owned workspace and state-file paths without mutating process-global environment
7
- //! - name: mcp_state_path_provenance
8
- //! what: records raw/canonical workspace, resolved state path, and filesystem ownership facts
9
- //! depends:
10
- //! - crate::mcp_server::lifecycle_tools::state_status
11
- //! - crate::state::selector
12
- //! boundary:
13
- //! - the owned no-compaction test writes only within its fixture root
14
- //! - this suite does not alter persistence semantics
15
- //! maturity: wired
16
- //! ---
17
- //! step 14a · mcp_server::tests — WAVE-2 RED contracts (Python v0.2.11 golden).
1
+ //! MCP server unit-test fixtures and focused stdio/wire contract tests.
18
2
  #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
19
3
 
20
4
  use super::*;
21
5
  use serde_json::json;
22
- use sha2::{Digest, Sha256};
23
6
  use std::path::{Path, PathBuf};
24
7
 
25
8
  // ── helpers ──────────────────────────────────────────────────────────────
@@ -35,11 +18,9 @@ fn keys(v: &Value) -> Vec<String> {
35
18
  v.as_object().unwrap().keys().cloned().collect()
36
19
  }
37
20
 
38
- /// A UNIQUE throwaway workspace dir per test (mirrors the state/coordinator idiom):
39
- /// tests that open the db (MessageStore) or write the filesystem MUST NOT share
40
- /// `/tmp/ws`, or they flake under parallel cargo (sqlite "database is locked" / NotFound).
41
- /// Pure-function / dispatch-shape tests that never touch fs/db keep a dummy fixed path.
42
- fn unique_ws(tag: &str) -> std::path::PathBuf {
21
+ /// A UNIQUE throwaway workspace dir per test. Filesystem and database tests
22
+ /// must not share `/tmp/ws` under parallel cargo.
23
+ fn unique_ws(tag: &str) -> PathBuf {
43
24
  use std::io::ErrorKind;
44
25
  use std::sync::atomic::{AtomicU64, Ordering};
45
26
  static N: AtomicU64 = AtomicU64::new(0);
@@ -54,183 +35,6 @@ fn unique_ws(tag: &str) -> std::path::PathBuf {
54
35
  }
55
36
  }
56
37
 
57
- static MCP_FIXTURE_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
58
-
59
- /// One process-owned root for update_state tests. All paths are passed explicitly
60
- /// to the code under test; the fixture never changes process-global environment.
61
- struct McpStateFixture {
62
- root: PathBuf,
63
- workspace: PathBuf,
64
- receipt: PathBuf,
65
- }
66
-
67
- impl McpStateFixture {
68
- fn new(_tag: &str) -> Self {
69
- let seq = MCP_FIXTURE_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
70
- let base = if cfg!(target_os = "macos") {
71
- PathBuf::from("/private/tmp")
72
- } else {
73
- PathBuf::from("/tmp")
74
- };
75
- let raw_root = base.join(format!("ta-mcp-{}-{seq}", std::process::id()));
76
- std::fs::create_dir(&raw_root).unwrap();
77
- let root = std::fs::canonicalize(raw_root).unwrap();
78
- let workspace = root.join("workspace");
79
- let receipt = root
80
- .parent()
81
- .unwrap()
82
- .join(format!("ta-mcp-receipt-{}-{seq}.json", std::process::id()));
83
- std::fs::create_dir(&workspace).unwrap();
84
-
85
- let fixture = Self {
86
- root,
87
- workspace,
88
- receipt,
89
- };
90
- fixture.seed_spec("team_state.md");
91
- crate::state::repository::StateRepository::new(&fixture.workspace)
92
- .save(
93
- crate::state::repository::StateWriteIntent::FakeE2eSeed,
94
- &json!({
95
- "session_name": "mcp-fixture",
96
- "active_team_key": "current",
97
- "agents": {},
98
- "tasks": []
99
- }),
100
- )
101
- .unwrap();
102
- fixture
103
- }
104
-
105
- fn seed_spec(&self, state_file: &str) {
106
- std::fs::write(
107
- self.workspace.join("team.spec.yaml"),
108
- format!(
109
- "team:\n name: mcp-fixture\n objective: path contract\nagents: []\ntasks: []\ncontext:\n state_file: {state_file}\n"
110
- ),
111
- )
112
- .unwrap();
113
- }
114
-
115
- fn state_file(&self, relative: &str) -> PathBuf {
116
- self.workspace.join(relative)
117
- }
118
-
119
- fn record_provenance(&self, raw_workspace: &Path, state_file: &Path) -> PathBuf {
120
- let canonical_workspace =
121
- crate::model::paths::canonical_run_workspace(raw_workspace).unwrap();
122
- assert!(self.under_root(raw_workspace));
123
- assert!(self.under_root(&canonical_workspace));
124
- assert!(self.under_root(state_file));
125
- let team_dir = self.workspace.join(".team");
126
- let runtime_dir = team_dir.join("runtime");
127
- let runtime_state = crate::state::persist::runtime_state_path(&self.workspace);
128
- let paths = [
129
- ("root", self.root.as_path()),
130
- ("raw_workspace", raw_workspace),
131
- ("canonical_workspace", canonical_workspace.as_path()),
132
- ("team_dir", team_dir.as_path()),
133
- ("runtime_dir", runtime_dir.as_path()),
134
- ("state_parent", state_file.parent().unwrap()),
135
- ("runtime_state", runtime_state.as_path()),
136
- ];
137
- let mut facts = paths
138
- .into_iter()
139
- .map(|(name, path)| {
140
- let metadata = std::fs::symlink_metadata(path).unwrap();
141
- json!({
142
- "name": name,
143
- "path": path.display().to_string(),
144
- "uid": metadata_uid(&metadata),
145
- "mode": metadata_mode(&metadata),
146
- "device": metadata_device(&metadata)
147
- })
148
- })
149
- .collect::<Vec<_>>();
150
- if let Ok(metadata) = std::fs::symlink_metadata(state_file) {
151
- facts.push(json!({
152
- "name": "state_file",
153
- "path": state_file.display().to_string(),
154
- "uid": metadata_uid(&metadata),
155
- "mode": metadata_mode(&metadata),
156
- "device": metadata_device(&metadata)
157
- }));
158
- }
159
- let payload = json!({
160
- "raw_workspace": raw_workspace.display().to_string(),
161
- "canonical_workspace": canonical_workspace.display().to_string(),
162
- "resolved_state_file": state_file.display().to_string(),
163
- "runtime_state_path": runtime_state.display().to_string(),
164
- "facts": facts
165
- });
166
- let payload_bytes = serde_json::to_vec(&payload).unwrap();
167
- let payload_sha256 = format!("{:x}", Sha256::digest(&payload_bytes));
168
- let receipt = json!({
169
- "schema": "mcp-state-provenance-v1",
170
- "payload": payload,
171
- "payload_sha256": payload_sha256
172
- });
173
- let receipt_bytes = serde_json::to_vec_pretty(&receipt).unwrap();
174
- use std::io::Write;
175
- let mut file = std::fs::OpenOptions::new()
176
- .write(true)
177
- .create_new(true)
178
- .open(&self.receipt)
179
- .unwrap();
180
- file.write_all(&receipt_bytes).unwrap();
181
- file.sync_all().unwrap();
182
- self.receipt.clone()
183
- }
184
-
185
- fn under_root(&self, path: &Path) -> bool {
186
- let candidate = if path.exists() {
187
- std::fs::canonicalize(path).unwrap()
188
- } else {
189
- path.to_path_buf()
190
- };
191
- candidate.starts_with(&self.root)
192
- }
193
- }
194
-
195
- impl Drop for McpStateFixture {
196
- fn drop(&mut self) {
197
- let _ = std::fs::remove_dir_all(&self.root);
198
- }
199
- }
200
-
201
- #[cfg(unix)]
202
- fn metadata_uid(metadata: &std::fs::Metadata) -> u32 {
203
- use std::os::unix::fs::MetadataExt;
204
- metadata.uid()
205
- }
206
-
207
- #[cfg(not(unix))]
208
- fn metadata_uid(_: &std::fs::Metadata) -> u32 {
209
- 0
210
- }
211
-
212
- #[cfg(unix)]
213
- fn metadata_mode(metadata: &std::fs::Metadata) -> u32 {
214
- use std::os::unix::fs::MetadataExt;
215
- metadata.mode()
216
- }
217
-
218
- #[cfg(not(unix))]
219
- fn metadata_mode(_: &std::fs::Metadata) -> u32 {
220
- 0
221
- }
222
-
223
- #[cfg(unix)]
224
- fn metadata_device(metadata: &std::fs::Metadata) -> u64 {
225
- use std::os::unix::fs::MetadataExt;
226
- metadata.dev()
227
- }
228
-
229
- #[cfg(not(unix))]
230
- fn metadata_device(_: &std::fs::Metadata) -> u64 {
231
- 0
232
- }
233
-
234
38
  include!("tests/normalize.rs");
235
39
  include!("tests/wire.rs");
236
40
  include!("tests/send.rs");
@@ -1,5 +1,5 @@
1
1
  //!
2
- //! step 14a · mcp_server::tools — `TeamOrchestratorTools`, the 12 typed handlers.
2
+ //! step 14a · mcp_server::tools — `TeamOrchestratorTools`, the three MCP handlers.
3
3
 
4
4
  use std::path::{Path, PathBuf};
5
5
 
@@ -13,16 +13,14 @@ use crate::model::ids::{AgentId, TaskId, TeamKey};
13
13
  use crate::event_log::EventLog;
14
14
 
15
15
  // ── REUSE: step 5 state persist / projection ────────────────────────────────
16
- use crate::state::persist::{
17
- load_runtime_state, save_runtime_state, save_runtime_state_reapplying_after_conflict,
18
- };
16
+ use crate::state::persist::load_runtime_state;
19
17
 
20
18
  // ── REUSE: step 11 messaging delegate surface ───────────────────────────────
21
19
  use crate::messaging::{self, DeliveryStatus, MessageTarget, SendOptions, TrustedSender};
22
20
 
23
21
  use super::helpers::{
24
22
  current_reportable_message_for, delivery_outcome_value, direct_message_attribution_for,
25
- ensure_object, enum_value, is_worker_recipient, json_dumps_default, latest_task_for_assignee,
23
+ ensure_object, enum_value, is_worker_recipient, latest_task_for_assignee,
26
24
  non_empty_string, object_fields, requires_ack_for_target, tool_runtime_error,
27
25
  DirectMessageAttribution,
28
26
  };
@@ -35,7 +33,7 @@ use super::types::{
35
33
  };
36
34
 
37
35
  // ═══════════════════════════════════════════════════════════════════════════
38
- // TeamOrchestratorTools (tools.py:72) — the 12 typed tool handlers.
36
+ // TeamOrchestratorTools — the three typed MCP handlers.
39
37
  // Identity/scope anchored on spawn-time env (TEAM_AGENT_ID / TEAM_AGENT_OWNER_TEAM_ID);
40
38
  // every handler delegates to runtime/MessageStore/EventLog. These ARE the
41
39
  // contract-callable behavioral entry fns.
@@ -183,100 +181,6 @@ impl TeamOrchestratorTools {
183
181
  error
184
182
  }
185
183
 
186
- /// `assign_task` (`tools.py:84-133`): C8 Family-B task-view reconcile then deliver.
187
- /// Resolves team key from owner-team env (or `active_team_key`), appends or
188
- /// field-updates the task in state, then delegates delivery to
189
- /// [`Self::send_message`] and compacts the result.
190
- pub fn assign_task(&self, task: &Value, message: Option<&str>) -> ToolResult {
191
- self.validate_rpc_scope_args("assign_task", task)?;
192
- let Some(task_obj) = task.as_object() else {
193
- return Err(ToolError::new(
194
- ToolErrorReason::InvalidToolArguments,
195
- "assign_task task must be an object",
196
- "ValueError",
197
- ));
198
- };
199
- let Some(task_id) = task
200
- .get("id")
201
- .and_then(Value::as_str)
202
- .and_then(non_empty_string)
203
- else {
204
- return Err(ToolError::new(
205
- ToolErrorReason::InvalidToolArguments,
206
- "assign_task task.id is required",
207
- "ValueError",
208
- ));
209
- };
210
- let Some(assignee) = task
211
- .get("assignee")
212
- .and_then(Value::as_str)
213
- .and_then(non_empty_string)
214
- else {
215
- return Err(ToolError::new(
216
- ToolErrorReason::InvalidToolArguments,
217
- "assign_task task.assignee is required",
218
- "ValueError",
219
- ));
220
- };
221
- if let Some(route) = task.get("result_route") {
222
- let Some(route) = route.as_str() else {
223
- return Err(ToolError::new(
224
- ToolErrorReason::InvalidToolArguments,
225
- "assign_task task.result_route must be 'leader' or 'pipeline'",
226
- "ValueError",
227
- ));
228
- };
229
- if crate::messaging::results::ResultRoute::parse(route).is_none() {
230
- return Err(ToolError::new(
231
- ToolErrorReason::InvalidToolArguments,
232
- format!("assign_task task.result_route has unknown value: {route}"),
233
- "ValueError",
234
- ));
235
- }
236
- }
237
-
238
- let task_value = Value::Object(task_obj.clone());
239
- let recovery = task_recovery_marker(&task_value);
240
- let mut state = load_runtime_state(&self.workspace).map_err(tool_runtime_error)?;
241
- ensure_object(&mut state);
242
- let team_key = self
243
- .canonical_owner_team_key()?
244
- .map(|team| team.as_str().to_string())
245
- .or_else(|| assignment_team_key(&state));
246
- crate::state::repository::StateRepository::new(&self.workspace)
247
- .commit(
248
- crate::state::repository::StateWriteIntent::McpAssignTask {
249
- team_key: team_key.as_deref(),
250
- task_id,
251
- },
252
- |latest| {
253
- ensure_object(latest);
254
- let latest_team_key = team_key.clone().or_else(|| assignment_team_key(latest));
255
- reconcile_assigned_task(latest, latest_team_key.as_deref(), &task_value);
256
- },
257
- )
258
- .map_err(tool_runtime_error)?;
259
-
260
- let content = assignment_message(task, message);
261
- let out = self.send_message(
262
- &MessageTarget::Single(assignee.to_string()),
263
- &content,
264
- Some(task_id),
265
- None,
266
- None,
267
- )?;
268
- let mut ok = compact_tool_result(&out.to_value())?;
269
- if recovery {
270
- ok.fields
271
- .insert("recovery".to_string(), serde_json::json!(true));
272
- ok.fields.insert(
273
- "acceptance_marker".to_string(),
274
- Value::String("recovery".to_string()),
275
- );
276
- }
277
- Ok(ok)
278
- }
279
-
280
184
  /// `send_message` (`tools.py:135-183`): C14/C15/C17 scope resolution.
281
185
  /// - sender = immutable `TEAM_AGENT_ID` captured when the MCP server starts.
282
186
  /// - `requires_ack` defaults from target (`_requires_ack_for_target`).
@@ -722,207 +626,12 @@ impl TeamOrchestratorTools {
722
626
  .and_then(|value| compact_tool_result(&value))
723
627
  }
724
628
 
725
- /// `update_state` (`tools.py:316-325`): delegated through the lifecycle tools
726
- /// facade. S0 preserves the old placeholder behavior.
727
- pub fn update_state(&self, note: &str) -> ToolResult {
728
- let owner_team = self.canonical_owner_team_key()?;
729
- super::lifecycle_tools::update_state(&self.workspace, owner_team.as_ref(), note)
730
- }
731
-
732
- /// `get_team_status` (`tools.py:327-328`): delegated through the lifecycle tools
733
- /// facade. S0 preserves the old placeholder behavior.
629
+ /// `get_team_status`: return the owner-team-scoped machine-readable status.
734
630
  pub fn get_team_status(&self) -> ToolResult {
735
631
  let owner_team = self.canonical_owner_team_key()?;
736
632
  super::lifecycle_tools::get_team_status(&self.workspace, owner_team.as_ref())
737
633
  }
738
634
 
739
- /// `stop_agent` (`tools.py:330-331`): delegated through the lifecycle tools facade.
740
- pub fn stop_agent(&self, agent_id: &str) -> ToolResult {
741
- let owner_team = self.canonical_owner_team_key()?;
742
- super::lifecycle_tools::stop_agent(&self.workspace, owner_team.as_ref(), agent_id)
743
- }
744
-
745
- /// `reset_agent` (`tools.py:333-334`): delegated through the lifecycle tools facade.
746
- pub fn reset_agent(&self, agent_id: &str, discard_session: bool) -> ToolResult {
747
- let owner_team = self.canonical_owner_team_key()?;
748
- super::lifecycle_tools::reset_agent(
749
- &self.workspace,
750
- owner_team.as_ref(),
751
- agent_id,
752
- discard_session,
753
- )
754
- }
755
-
756
- /// `add_agent` (`tools.py:336-337`): delegate to real lifecycle add-agent
757
- /// under the spawn-time owner team.
758
- pub fn add_agent(&self, new_agent_id: &str, role_file_path: &str) -> ToolResult {
759
- let owner_team = self
760
- .canonical_owner_team_key()?
761
- .ok_or_else(|| self.scope_refused("add_agent requires TEAM_AGENT_OWNER_TEAM_ID"))?;
762
- let role_file = Path::new(role_file_path);
763
- let role_file = if role_file.is_absolute() {
764
- role_file.to_path_buf()
765
- } else {
766
- self.workspace.join(role_file)
767
- };
768
- crate::lifecycle::launch::add_agent(
769
- &self.workspace,
770
- &AgentId::new(new_agent_id.to_string()),
771
- &role_file,
772
- false,
773
- Some(owner_team.as_str()),
774
- )
775
- .map_err(tool_runtime_error)
776
- .and_then(|report| {
777
- compact_tool_result(&serde_json::json!({
778
- "ok": true,
779
- "status": "added",
780
- "agent_id": new_agent_id,
781
- "state_file": report.env.state_file.to_string_lossy().to_string(),
782
- "coordinator_started": report.env.coordinator_started,
783
- "start_mode": format!("{:?}", report.start_mode),
784
- "role_file": report.role_file.to_string_lossy().to_string(),
785
- }))
786
- })
787
- }
788
-
789
- /// `fork_agent` (`tools.py:339-340`): delegated through the lifecycle tools facade.
790
- pub fn fork_agent(
791
- &self,
792
- source_agent_id: &str,
793
- as_agent_id: &str,
794
- label: Option<&str>,
795
- ) -> ToolResult {
796
- let owner_team = self.canonical_owner_team_key()?;
797
- super::lifecycle_tools::fork_agent(
798
- &self.workspace,
799
- owner_team.as_ref(),
800
- source_agent_id,
801
- as_agent_id,
802
- label,
803
- )
804
- }
805
-
806
- pub fn clone_agent(
807
- &self,
808
- source_agent_id: &str,
809
- as_agent_id: &str,
810
- label: Option<&str>,
811
- ) -> ToolResult {
812
- let owner_team = self.canonical_owner_team_key()?;
813
- super::lifecycle_tools::clone_agent(
814
- &self.workspace,
815
- owner_team.as_ref(),
816
- source_agent_id,
817
- as_agent_id,
818
- label,
819
- )
820
- }
821
-
822
- /// `request_human` (`tools.py:342-346`): create a `requires_ack` leader message via
823
- /// the shared leader-delivery funnel; sender = env / inferred / `"unknown"`.
824
- /// Returns `{ok:true, message_id, status:"needs_human"}`.
825
- pub fn request_human(
826
- &self,
827
- question: &str,
828
- task_id: Option<&str>,
829
- agent_id: Option<&str>,
830
- ) -> ToolResult {
831
- let _owner_team = self.canonical_owner_team_key()?;
832
- let explicit_sender = agent_id.and_then(non_empty_string);
833
- let sender = explicit_sender
834
- .or_else(|| self.agent_id.as_ref().map(AgentId::as_str))
835
- .unwrap_or("unknown");
836
- let event_log = EventLog::new(&self.workspace);
837
- if explicit_sender.is_none() && self.agent_id.is_none() {
838
- event_log
839
- .write(
840
- "mcp.identity_inference_failed",
841
- serde_json::json!({"tool": "request_human"}),
842
- )
843
- .map_err(tool_runtime_error)?;
844
- }
845
- // #230 N31/N32 funnel: request_human is a leader-bound caller and must go through
846
- // the same primitive as send_message(to=leader) / report_result / idle reminder.
847
- // The legacy path was a raw store insert for recipient="leader" that
848
- // bypassed the leader-delivery audit (no deliver_to_leader.submit emit, no rebind
849
- // guard, no leader_notification_log dedup). funnel it now.
850
- let state = crate::state::persist::load_runtime_state(&self.workspace)
851
- .unwrap_or(serde_json::json!({}));
852
- let task = task_id.map(|t| TaskId::new(t.to_string()));
853
- let outcome = crate::messaging::send_to_leader_receiver(
854
- &self.workspace,
855
- &state,
856
- "leader",
857
- question,
858
- task.as_ref(),
859
- sender,
860
- true,
861
- None,
862
- &event_log,
863
- )
864
- .map_err(tool_runtime_error)?;
865
- let mut fields = serde_json::Map::new();
866
- fields.insert("ok".to_string(), Value::Bool(outcome.ok));
867
- fields.insert(
868
- "message_id".to_string(),
869
- outcome
870
- .message_id
871
- .clone()
872
- .map_or(Value::Null, Value::String),
873
- );
874
- fields.insert(
875
- "status".to_string(),
876
- Value::String("needs_human".to_string()),
877
- );
878
- Ok(ToolOk { fields })
879
- }
880
-
881
- /// `stuck_list` (`tools.py:348-349`): delegate to [`messaging::stuck_list`] (the
882
- /// team-scoped suppressed-alert projection).
883
- pub fn stuck_list(&self) -> ToolResult {
884
- let _owner_team = self.canonical_owner_team_key()?;
885
- messaging::stuck_list(&self.workspace)
886
- .map_err(tool_runtime_error)
887
- .map(|v| ToolOk {
888
- fields: object_fields(v),
889
- })
890
- }
891
-
892
- /// `stuck_cancel` (`tools.py:351-352`): delegate to [`messaging::stuck_cancel`];
893
- /// `suppressed_by` = env agent_id / `"leader"`.
894
- pub fn stuck_cancel(&self, agent_id: &str, alert_type: &str) -> ToolResult {
895
- let _owner_team = self.canonical_owner_team_key()?;
896
- let alert = match alert_type {
897
- "stuck" => Some(messaging::AlertType::Stuck),
898
- "idle_fallback" => Some(messaging::AlertType::IdleFallback),
899
- "cross_worker_deadlock" => Some(messaging::AlertType::CrossWorkerDeadlock),
900
- "all" => None,
901
- // scheduler.py:268-273 — an unknown alert_type refuses with the Python
902
- // literal instead of silently widening to suppressing ALL alert families.
903
- _ => {
904
- return Ok(ToolOk {
905
- fields: object_fields(serde_json::json!({
906
- "ok": false,
907
- "status": "refused",
908
- "reason": "invalid_alert_type",
909
- "alert_type": alert_type,
910
- })),
911
- });
912
- }
913
- };
914
- let suppressed_by = self
915
- .agent_id
916
- .as_ref()
917
- .map(AgentId::as_str)
918
- .unwrap_or("leader");
919
- messaging::stuck_cancel(&self.workspace, agent_id, alert, suppressed_by)
920
- .map_err(tool_runtime_error)
921
- .map(|v| ToolOk {
922
- fields: object_fields(v),
923
- })
924
- }
925
-
926
635
  /// `get_visible_peers` (`tools.py:226-247`): C16 scope-filtered peer list — live
927
636
  /// agents within the spawn-time owner-team scope only; other teams and dead/stopped
928
637
  /// agents are filtered server-side and never named.
@@ -1275,63 +984,6 @@ fn requested_scope_arg(args: &Value) -> Option<String> {
1275
984
  .or_else(|| args.get("workspace").map(|_| "workspace".to_string()))
1276
985
  }
1277
986
 
1278
- fn assignment_team_key(state: &Value) -> Option<String> {
1279
- state
1280
- .get("active_team_key")
1281
- .and_then(Value::as_str)
1282
- .and_then(non_empty_string)
1283
- .map(ToString::to_string)
1284
- }
1285
-
1286
- fn reconcile_assigned_task(state: &mut Value, team_key: Option<&str>, task: &Value) {
1287
- let mut top = state
1288
- .get("tasks")
1289
- .and_then(Value::as_array)
1290
- .cloned()
1291
- .unwrap_or_default();
1292
- upsert_task_in_place(&mut top, task);
1293
- if let Some(root) = state.as_object_mut() {
1294
- root.insert("tasks".to_string(), Value::Array(top.clone()));
1295
- }
1296
- if let Some(key) = team_key {
1297
- let mut team_tasks = state
1298
- .get("teams")
1299
- .and_then(|v| v.get(key))
1300
- .and_then(|team| team.get("tasks"))
1301
- .and_then(Value::as_array)
1302
- .cloned()
1303
- .unwrap_or_default();
1304
- upsert_task_in_place(&mut team_tasks, task);
1305
- write_team_tasks(state, key, team_tasks);
1306
- }
1307
- }
1308
-
1309
- fn upsert_task_in_place(tasks: &mut Vec<Value>, task: &Value) {
1310
- let Some(task_id) = task.get("id").and_then(Value::as_str) else {
1311
- return;
1312
- };
1313
- for existing in tasks.iter_mut() {
1314
- if existing.get("id").and_then(Value::as_str) == Some(task_id) {
1315
- merge_object_fields(existing, task);
1316
- return;
1317
- }
1318
- }
1319
- tasks.push(task.clone());
1320
- }
1321
-
1322
- fn merge_object_fields(existing: &mut Value, incoming: &Value) {
1323
- let Some(existing_obj) = existing.as_object_mut() else {
1324
- *existing = incoming.clone();
1325
- return;
1326
- };
1327
- let Some(incoming_obj) = incoming.as_object() else {
1328
- return;
1329
- };
1330
- for (key, value) in incoming_obj {
1331
- existing_obj.insert(key.clone(), value.clone());
1332
- }
1333
- }
1334
-
1335
987
  fn push_report_warning(obj: &mut serde_json::Map<String, Value>, warning: Value) {
1336
988
  let Some(code) = warning.get("code").and_then(Value::as_str) else {
1337
989
  return;
@@ -1355,49 +1007,6 @@ fn push_report_warning(obj: &mut serde_json::Map<String, Value>, warning: Value)
1355
1007
  }
1356
1008
  }
1357
1009
 
1358
- fn write_team_tasks(state: &mut Value, team_key: &str, tasks: Vec<Value>) {
1359
- let Some(root) = state.as_object_mut() else {
1360
- return;
1361
- };
1362
- let teams = root
1363
- .entry("teams".to_string())
1364
- .or_insert_with(|| Value::Object(serde_json::Map::new()));
1365
- let Some(teams_obj) = teams.as_object_mut() else {
1366
- return;
1367
- };
1368
- let team = teams_obj.entry(team_key.to_string()).or_insert_with(|| {
1369
- let mut team = serde_json::Map::new();
1370
- team.insert("tasks".to_string(), Value::Array(Vec::new()));
1371
- team.insert("status".to_string(), Value::String("alive".to_string()));
1372
- Value::Object(team)
1373
- });
1374
- let Some(team_obj) = team.as_object_mut() else {
1375
- return;
1376
- };
1377
- team_obj.insert("tasks".to_string(), Value::Array(tasks));
1378
- }
1379
-
1380
- fn assignment_message(task: &Value, explicit: Option<&str>) -> String {
1381
- if let Some(message) = explicit.and_then(non_empty_string) {
1382
- return message.to_string();
1383
- }
1384
- for key in ["description", "title"] {
1385
- if let Some(text) = task
1386
- .get(key)
1387
- .and_then(Value::as_str)
1388
- .and_then(non_empty_string)
1389
- {
1390
- return text.to_string();
1391
- }
1392
- }
1393
- json_dumps_default(task)
1394
- }
1395
-
1396
- fn task_recovery_marker(task: &Value) -> bool {
1397
- task.get("recovery").and_then(Value::as_bool) == Some(true)
1398
- || task.get("acceptance_marker").and_then(Value::as_str) == Some("recovery")
1399
- }
1400
-
1401
1010
  fn scope_override_name(scope: Scope) -> Option<&'static str> {
1402
1011
  match scope {
1403
1012
  Scope::Team => Some("team"),