@team-agent/installer 0.2.11 → 0.3.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 (326) hide show
  1. package/Cargo.lock +744 -0
  2. package/Cargo.toml +34 -0
  3. package/crates/team-agent/Cargo.toml +33 -0
  4. package/crates/team-agent/src/cli/adapters.rs +1343 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +554 -0
  6. package/crates/team-agent/src/cli/emit.rs +1077 -0
  7. package/crates/team-agent/src/cli/helpers.rs +88 -0
  8. package/crates/team-agent/src/cli/leader.rs +216 -0
  9. package/crates/team-agent/src/cli/mod.rs +1141 -0
  10. package/crates/team-agent/src/cli/profile.rs +306 -0
  11. package/crates/team-agent/src/cli/send.rs +215 -0
  12. package/crates/team-agent/src/cli/status.rs +179 -0
  13. package/crates/team-agent/src/cli/status_port.rs +502 -0
  14. package/crates/team-agent/src/cli/tests/base.rs +616 -0
  15. package/crates/team-agent/src/cli/tests/compile.rs +96 -0
  16. package/crates/team-agent/src/cli/tests/divergence.rs +509 -0
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +333 -0
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +395 -0
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +675 -0
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +390 -0
  21. package/crates/team-agent/src/cli/tests/mod.rs +97 -0
  22. package/crates/team-agent/src/cli/tests/peer_allow.rs +137 -0
  23. package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +302 -0
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +305 -0
  25. package/crates/team-agent/src/cli/tests/status_send.rs +385 -0
  26. package/crates/team-agent/src/cli/tests/verb_profile.rs +182 -0
  27. package/crates/team-agent/src/cli/tests/verb_settle.rs +236 -0
  28. package/crates/team-agent/src/cli/tests/verb_validate.rs +184 -0
  29. package/crates/team-agent/src/cli/types.rs +605 -0
  30. package/crates/team-agent/src/compiler/tests.rs +701 -0
  31. package/crates/team-agent/src/compiler.rs +489 -0
  32. package/crates/team-agent/src/coordinator/backoff.rs +153 -0
  33. package/crates/team-agent/src/coordinator/health.rs +436 -0
  34. package/crates/team-agent/src/coordinator/mod.rs +80 -0
  35. package/crates/team-agent/src/coordinator/orphan.rs +179 -0
  36. package/crates/team-agent/src/coordinator/tests/abnormal.rs +255 -0
  37. package/crates/team-agent/src/coordinator/tests/basics.rs +262 -0
  38. package/crates/team-agent/src/coordinator/tests/daemon.rs +323 -0
  39. package/crates/team-agent/src/coordinator/tests/health_sync.rs +263 -0
  40. package/crates/team-agent/src/coordinator/tests/main_preserved.rs +136 -0
  41. package/crates/team-agent/src/coordinator/tests/mod.rs +310 -0
  42. package/crates/team-agent/src/coordinator/tests/spine.rs +261 -0
  43. package/crates/team-agent/src/coordinator/tests/takeover.rs +227 -0
  44. package/crates/team-agent/src/coordinator/tests/tick_core.rs +256 -0
  45. package/crates/team-agent/src/coordinator/tests/watch.rs +167 -0
  46. package/crates/team-agent/src/coordinator/tick.rs +2032 -0
  47. package/crates/team-agent/src/coordinator/types.rs +584 -0
  48. package/crates/team-agent/src/db/migration.rs +716 -0
  49. package/crates/team-agent/src/db/mod.rs +23 -0
  50. package/crates/team-agent/src/db/schema.rs +378 -0
  51. package/crates/team-agent/src/event_log.rs +375 -0
  52. package/crates/team-agent/src/fake_worker.rs +253 -0
  53. package/crates/team-agent/src/leader/helpers.rs +190 -0
  54. package/crates/team-agent/src/leader/inject.rs +33 -0
  55. package/crates/team-agent/src/leader/lease.rs +1063 -0
  56. package/crates/team-agent/src/leader/mod.rs +99 -0
  57. package/crates/team-agent/src/leader/owner_bind.rs +292 -0
  58. package/crates/team-agent/src/leader/rediscover/tests.rs +525 -0
  59. package/crates/team-agent/src/leader/rediscover.rs +1099 -0
  60. package/crates/team-agent/src/leader/start.rs +273 -0
  61. package/crates/team-agent/src/leader/takeover.rs +235 -0
  62. package/crates/team-agent/src/leader/tests/basics.rs +183 -0
  63. package/crates/team-agent/src/leader/tests/byte_findings.rs +234 -0
  64. package/crates/team-agent/src/leader/tests/identity.rs +206 -0
  65. package/crates/team-agent/src/leader/tests/idle.rs +271 -0
  66. package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
  67. package/crates/team-agent/src/leader/tests/lease_claim.rs +253 -0
  68. package/crates/team-agent/src/leader/tests/mod.rs +125 -0
  69. package/crates/team-agent/src/leader/tests/rediscover.rs +351 -0
  70. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +204 -0
  71. package/crates/team-agent/src/leader/types.rs +487 -0
  72. package/crates/team-agent/src/lib.rs +85 -0
  73. package/crates/team-agent/src/lifecycle/display.rs +228 -0
  74. package/crates/team-agent/src/lifecycle/helpers.rs +112 -0
  75. package/crates/team-agent/src/lifecycle/launch/plan.rs +227 -0
  76. package/crates/team-agent/src/lifecycle/launch.rs +1833 -0
  77. package/crates/team-agent/src/lifecycle/mod.rs +62 -0
  78. package/crates/team-agent/src/lifecycle/restart/agent.rs +533 -0
  79. package/crates/team-agent/src/lifecycle/restart/common.rs +517 -0
  80. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +41 -0
  81. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +268 -0
  82. package/crates/team-agent/src/lifecycle/restart/remove.rs +780 -0
  83. package/crates/team-agent/src/lifecycle/restart/selection.rs +208 -0
  84. package/crates/team-agent/src/lifecycle/restart/team_state.rs +242 -0
  85. package/crates/team-agent/src/lifecycle/restart.rs +76 -0
  86. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +455 -0
  87. package/crates/team-agent/src/lifecycle/tests/core.rs +989 -0
  88. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +583 -0
  89. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +933 -0
  90. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +265 -0
  91. package/crates/team-agent/src/lifecycle/tests.rs +27 -0
  92. package/crates/team-agent/src/lifecycle/types.rs +685 -0
  93. package/crates/team-agent/src/main.rs +41 -0
  94. package/crates/team-agent/src/mcp_server/helpers.rs +228 -0
  95. package/crates/team-agent/src/mcp_server/mod.rs +183 -0
  96. package/crates/team-agent/src/mcp_server/normalize.rs +312 -0
  97. package/crates/team-agent/src/mcp_server/tests/golden.rs +283 -0
  98. package/crates/team-agent/src/mcp_server/tests/normalize.rs +244 -0
  99. package/crates/team-agent/src/mcp_server/tests/scoped.rs +189 -0
  100. package/crates/team-agent/src/mcp_server/tests/send.rs +222 -0
  101. package/crates/team-agent/src/mcp_server/tests/tools.rs +158 -0
  102. package/crates/team-agent/src/mcp_server/tests/wire.rs +159 -0
  103. package/crates/team-agent/src/mcp_server/tests.rs +38 -0
  104. package/crates/team-agent/src/mcp_server/tools.rs +603 -0
  105. package/crates/team-agent/src/mcp_server/types.rs +421 -0
  106. package/crates/team-agent/src/mcp_server/wire.rs +388 -0
  107. package/crates/team-agent/src/message_store.rs +767 -0
  108. package/crates/team-agent/src/messaging/activity.rs +433 -0
  109. package/crates/team-agent/src/messaging/delivery.rs +542 -0
  110. package/crates/team-agent/src/messaging/helpers.rs +209 -0
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +340 -0
  112. package/crates/team-agent/src/messaging/mod.rs +147 -0
  113. package/crates/team-agent/src/messaging/peers.rs +32 -0
  114. package/crates/team-agent/src/messaging/results.rs +537 -0
  115. package/crates/team-agent/src/messaging/scheduler.rs +344 -0
  116. package/crates/team-agent/src/messaging/selftest.rs +100 -0
  117. package/crates/team-agent/src/messaging/send.rs +582 -0
  118. package/crates/team-agent/src/messaging/tests/basic.rs +357 -0
  119. package/crates/team-agent/src/messaging/tests/main_preserved.rs +122 -0
  120. package/crates/team-agent/src/messaging/tests/mod.rs +293 -0
  121. package/crates/team-agent/src/messaging/tests/runtime.rs +1422 -0
  122. package/crates/team-agent/src/messaging/tests/spine.rs +437 -0
  123. package/crates/team-agent/src/messaging/trust.rs +192 -0
  124. package/crates/team-agent/src/messaging/types.rs +355 -0
  125. package/crates/team-agent/src/messaging/watchers.rs +591 -0
  126. package/crates/team-agent/src/model/enums.rs +311 -0
  127. package/crates/team-agent/src/model/errors.rs +17 -0
  128. package/crates/team-agent/src/model/ids.rs +155 -0
  129. package/crates/team-agent/src/model/mod.rs +22 -0
  130. package/crates/team-agent/src/model/paths.rs +228 -0
  131. package/crates/team-agent/src/model/permissions.rs +567 -0
  132. package/crates/team-agent/src/model/routing.rs +340 -0
  133. package/crates/team-agent/src/model/spec.rs +680 -0
  134. package/crates/team-agent/src/model/task_graph.rs +380 -0
  135. package/crates/team-agent/src/model/testdata/fuzz.golden.yaml +43 -0
  136. package/crates/team-agent/src/model/testdata/fuzz.yaml +43 -0
  137. package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +207 -0
  138. package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +206 -0
  139. package/crates/team-agent/src/model/testdata/team.spec.yaml +206 -0
  140. package/crates/team-agent/src/model/yaml/tests.rs +288 -0
  141. package/crates/team-agent/src/model/yaml.rs +800 -0
  142. package/crates/team-agent/src/packaging/install.rs +305 -0
  143. package/crates/team-agent/src/packaging/migrate.rs +30 -0
  144. package/crates/team-agent/src/packaging/mod.rs +82 -0
  145. package/crates/team-agent/src/packaging/repair.rs +24 -0
  146. package/crates/team-agent/src/packaging/tests.rs +829 -0
  147. package/crates/team-agent/src/packaging/types.rs +369 -0
  148. package/crates/team-agent/src/provider/adapter.rs +801 -0
  149. package/crates/team-agent/src/provider/approvals/mod.rs +2 -0
  150. package/crates/team-agent/src/provider/approvals/parsing.rs +452 -0
  151. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +163 -0
  152. package/crates/team-agent/src/provider/classify.rs +456 -0
  153. package/crates/team-agent/src/provider/faults.rs +136 -0
  154. package/crates/team-agent/src/provider/helpers.rs +41 -0
  155. package/crates/team-agent/src/provider/mod.rs +53 -0
  156. package/crates/team-agent/src/provider/startup_prompt.rs +423 -0
  157. package/crates/team-agent/src/provider/tests/adapter.rs +239 -0
  158. package/crates/team-agent/src/provider/tests/classify.rs +240 -0
  159. package/crates/team-agent/src/provider/tests/faults.rs +120 -0
  160. package/crates/team-agent/src/provider/tests/idle.rs +208 -0
  161. package/crates/team-agent/src/provider/tests/wire.rs +213 -0
  162. package/crates/team-agent/src/provider/tests.rs +31 -0
  163. package/crates/team-agent/src/provider/types.rs +424 -0
  164. package/crates/team-agent/src/state/identity.rs +656 -0
  165. package/crates/team-agent/src/state/mod.rs +58 -0
  166. package/crates/team-agent/src/state/owner_gate.rs +423 -0
  167. package/crates/team-agent/src/state/persist.rs +712 -0
  168. package/crates/team-agent/src/state/projection.rs +657 -0
  169. package/crates/team-agent/src/state/selector.rs +105 -0
  170. package/crates/team-agent/src/state/testdata/state-rich.canonical.json +133 -0
  171. package/crates/team-agent/src/tmux_backend/tests.rs +586 -0
  172. package/crates/team-agent/src/tmux_backend.rs +758 -0
  173. package/crates/team-agent/src/transport/test_support.rs +252 -0
  174. package/crates/team-agent/src/transport/tests/behavior.rs +327 -0
  175. package/crates/team-agent/src/transport/tests/mod.rs +199 -0
  176. package/crates/team-agent/src/transport/tests/wire.rs +527 -0
  177. package/crates/team-agent/src/transport.rs +774 -0
  178. package/npm/install.mjs +90 -106
  179. package/package.json +15 -13
  180. package/crates/team-agent-core/Cargo.toml +0 -12
  181. package/crates/team-agent-core/src/lib.rs +0 -332
  182. package/crates/team-agent-core/src/main.rs +0 -152
  183. package/pyproject.toml +0 -18
  184. package/scripts/install.py +0 -88
  185. package/scripts/run_regression_tests.py +0 -83
  186. package/src/team_agent/__init__.py +0 -3
  187. package/src/team_agent/__main__.py +0 -5
  188. package/src/team_agent/_legacy_pane_discovery.py +0 -186
  189. package/src/team_agent/abnormal_track.py +0 -253
  190. package/src/team_agent/approvals/__init__.py +0 -65
  191. package/src/team_agent/approvals/constants.py +0 -6
  192. package/src/team_agent/approvals/parsing.py +0 -176
  193. package/src/team_agent/approvals/runtime_prompts.py +0 -171
  194. package/src/team_agent/approvals/status.py +0 -176
  195. package/src/team_agent/cli/__init__.py +0 -137
  196. package/src/team_agent/cli/commands.py +0 -481
  197. package/src/team_agent/cli/e2e.py +0 -202
  198. package/src/team_agent/cli/helpers.py +0 -226
  199. package/src/team_agent/cli/parser.py +0 -540
  200. package/src/team_agent/compiler.py +0 -334
  201. package/src/team_agent/coordinator/__init__.py +0 -53
  202. package/src/team_agent/coordinator/__main__.py +0 -119
  203. package/src/team_agent/coordinator/lifecycle.py +0 -411
  204. package/src/team_agent/coordinator/metadata.py +0 -61
  205. package/src/team_agent/coordinator/paths.py +0 -17
  206. package/src/team_agent/diagnose/__init__.py +0 -48
  207. package/src/team_agent/diagnose/checks.py +0 -101
  208. package/src/team_agent/diagnose/comms.py +0 -213
  209. package/src/team_agent/diagnose/health.py +0 -241
  210. package/src/team_agent/diagnose/orphan_cleanup.py +0 -364
  211. package/src/team_agent/diagnose/preflight.py +0 -194
  212. package/src/team_agent/diagnose/quick_start.py +0 -324
  213. package/src/team_agent/display/__init__.py +0 -92
  214. package/src/team_agent/display/adaptive.py +0 -511
  215. package/src/team_agent/display/backend.py +0 -46
  216. package/src/team_agent/display/close.py +0 -154
  217. package/src/team_agent/display/ghostty.py +0 -77
  218. package/src/team_agent/display/rebuild.py +0 -102
  219. package/src/team_agent/display/tiling.py +0 -156
  220. package/src/team_agent/display/worker_window.py +0 -114
  221. package/src/team_agent/display/workspace.py +0 -382
  222. package/src/team_agent/errors.py +0 -10
  223. package/src/team_agent/events.py +0 -84
  224. package/src/team_agent/fake_worker.py +0 -80
  225. package/src/team_agent/idle_predicate.py +0 -218
  226. package/src/team_agent/idle_takeover.py +0 -59
  227. package/src/team_agent/idle_takeover_wiring.py +0 -114
  228. package/src/team_agent/launch/__init__.py +0 -41
  229. package/src/team_agent/launch/bootstrap.py +0 -85
  230. package/src/team_agent/launch/config.py +0 -106
  231. package/src/team_agent/launch/core.py +0 -301
  232. package/src/team_agent/launch/requirements.py +0 -57
  233. package/src/team_agent/leader/__init__.py +0 -926
  234. package/src/team_agent/leader_binding.py +0 -183
  235. package/src/team_agent/lifecycle/__init__.py +0 -5
  236. package/src/team_agent/lifecycle/agents.py +0 -278
  237. package/src/team_agent/lifecycle/operations.py +0 -411
  238. package/src/team_agent/lifecycle/paste_buffer_hygiene.py +0 -39
  239. package/src/team_agent/lifecycle/start.py +0 -363
  240. package/src/team_agent/mcp_server/__init__.py +0 -42
  241. package/src/team_agent/mcp_server/__main__.py +0 -7
  242. package/src/team_agent/mcp_server/contracts.py +0 -148
  243. package/src/team_agent/mcp_server/normalize.py +0 -257
  244. package/src/team_agent/mcp_server/server.py +0 -150
  245. package/src/team_agent/mcp_server/tools.py +0 -352
  246. package/src/team_agent/message_store/__init__.py +0 -23
  247. package/src/team_agent/message_store/agent_health.py +0 -113
  248. package/src/team_agent/message_store/core.py +0 -497
  249. package/src/team_agent/message_store/leader_notification_log.py +0 -198
  250. package/src/team_agent/message_store/result_watchers.py +0 -251
  251. package/src/team_agent/message_store/schema.py +0 -308
  252. package/src/team_agent/message_store/schema_migration.py +0 -448
  253. package/src/team_agent/messaging/__init__.py +0 -1
  254. package/src/team_agent/messaging/activity_detector.py +0 -262
  255. package/src/team_agent/messaging/delivery.py +0 -504
  256. package/src/team_agent/messaging/deps.py +0 -247
  257. package/src/team_agent/messaging/idle_alerts.py +0 -423
  258. package/src/team_agent/messaging/internal_delivery.py +0 -46
  259. package/src/team_agent/messaging/leader.py +0 -497
  260. package/src/team_agent/messaging/leader_api_errors.py +0 -216
  261. package/src/team_agent/messaging/leader_panes.py +0 -673
  262. package/src/team_agent/messaging/owner_bypass.py +0 -29
  263. package/src/team_agent/messaging/result_delivery.py +0 -539
  264. package/src/team_agent/messaging/results.py +0 -447
  265. package/src/team_agent/messaging/scheduler.py +0 -450
  266. package/src/team_agent/messaging/send.py +0 -532
  267. package/src/team_agent/messaging/session_drift.py +0 -94
  268. package/src/team_agent/messaging/tmux_io.py +0 -506
  269. package/src/team_agent/messaging/tmux_prompt.py +0 -338
  270. package/src/team_agent/messaging/trust_auto_answer.py +0 -52
  271. package/src/team_agent/orchestrator/__init__.py +0 -376
  272. package/src/team_agent/orchestrator/plan.py +0 -122
  273. package/src/team_agent/orchestrator/state.py +0 -128
  274. package/src/team_agent/paths.py +0 -45
  275. package/src/team_agent/permissions.py +0 -123
  276. package/src/team_agent/profiles/__init__.py +0 -82
  277. package/src/team_agent/profiles/constants.py +0 -19
  278. package/src/team_agent/profiles/core.py +0 -407
  279. package/src/team_agent/profiles/helpers.py +0 -69
  280. package/src/team_agent/profiles/provider_env.py +0 -188
  281. package/src/team_agent/profiles/smoke.py +0 -201
  282. package/src/team_agent/provider_cli/__init__.py +0 -43
  283. package/src/team_agent/provider_cli/adapter.py +0 -172
  284. package/src/team_agent/provider_cli/base.py +0 -48
  285. package/src/team_agent/provider_cli/claude.py +0 -503
  286. package/src/team_agent/provider_cli/codex.py +0 -336
  287. package/src/team_agent/provider_cli/copilot.py +0 -8
  288. package/src/team_agent/provider_cli/fake.py +0 -39
  289. package/src/team_agent/provider_cli/gemini.py +0 -95
  290. package/src/team_agent/provider_cli/opencode.py +0 -8
  291. package/src/team_agent/provider_cli/prompt.py +0 -62
  292. package/src/team_agent/provider_cli/registry.py +0 -18
  293. package/src/team_agent/provider_cli/unsupported.py +0 -32
  294. package/src/team_agent/provider_state/README.md +0 -78
  295. package/src/team_agent/provider_state/__init__.py +0 -91
  296. package/src/team_agent/provider_state/claude.py +0 -86
  297. package/src/team_agent/provider_state/codex.py +0 -84
  298. package/src/team_agent/provider_state/common.py +0 -207
  299. package/src/team_agent/provider_state/registry.py +0 -118
  300. package/src/team_agent/providers.py +0 -163
  301. package/src/team_agent/quality_gates.py +0 -104
  302. package/src/team_agent/restart/__init__.py +0 -34
  303. package/src/team_agent/restart/orchestration.py +0 -554
  304. package/src/team_agent/restart/selection.py +0 -89
  305. package/src/team_agent/restart/snapshot.py +0 -70
  306. package/src/team_agent/routing.py +0 -84
  307. package/src/team_agent/runtime.py +0 -1243
  308. package/src/team_agent/rust_core.py +0 -327
  309. package/src/team_agent/sessions/__init__.py +0 -25
  310. package/src/team_agent/sessions/capture.py +0 -144
  311. package/src/team_agent/sessions/inventory.py +0 -44
  312. package/src/team_agent/sessions/resume.py +0 -135
  313. package/src/team_agent/simple_yaml.py +0 -236
  314. package/src/team_agent/spec.py +0 -370
  315. package/src/team_agent/state.py +0 -693
  316. package/src/team_agent/status/__init__.py +0 -63
  317. package/src/team_agent/status/approvals.py +0 -52
  318. package/src/team_agent/status/compact.py +0 -158
  319. package/src/team_agent/status/constants.py +0 -18
  320. package/src/team_agent/status/inbox.py +0 -58
  321. package/src/team_agent/status/peek.py +0 -117
  322. package/src/team_agent/status/queries.py +0 -199
  323. package/src/team_agent/task_graph.py +0 -80
  324. package/src/team_agent/terminal.py +0 -57
  325. package/src/team_agent/wake.py +0 -58
  326. package/src/team_agent/watch/__init__.py +0 -145
@@ -0,0 +1,603 @@
1
+ //! step 14a · mcp_server::tools — `TeamOrchestratorTools`, the 12 typed handlers.
2
+
3
+ use std::path::{Path, PathBuf};
4
+
5
+ use serde_json::Value;
6
+
7
+ // ── REUSE: step 2 model (ids + normalized-envelope value enums) ─────────────
8
+ use crate::model::enums::ResultStatus;
9
+ use crate::model::ids::{AgentId, TaskId, TeamKey};
10
+
11
+ // ── REUSE: step 4 event_log / step 7 message_store ──────────────────────────
12
+ use crate::event_log::EventLog;
13
+ use crate::message_store::MessageStore;
14
+
15
+ // ── REUSE: step 5 state persist / projection ────────────────────────────────
16
+ use crate::state::persist::{load_runtime_state, save_runtime_state};
17
+
18
+ // ── REUSE: step 11 messaging delegate surface ───────────────────────────────
19
+ use crate::messaging::{self, MessageTarget, SendOptions};
20
+
21
+ use super::helpers::{
22
+ delivery_outcome_value, ensure_object, enum_value, insert_array, is_worker_recipient,
23
+ json_dumps_default, latest_task_for_assignee, non_empty_string, normalized_envelope_value, object_fields,
24
+ requires_ack_for_target, tool_runtime_error,
25
+ };
26
+ use super::normalize::{compact_tool_result, normalize_report_envelope};
27
+ use super::types::{McpError, Scope, SendOutcome, ToolError, ToolErrorReason, ToolOk, ToolResult, VisiblePeers};
28
+
29
+ // ═══════════════════════════════════════════════════════════════════════════
30
+ // TeamOrchestratorTools (tools.py:72) — the 12 typed tool handlers.
31
+ // Identity/scope anchored on spawn-time env (TEAM_AGENT_ID / TEAM_AGENT_OWNER_TEAM_ID);
32
+ // every handler delegates to runtime/MessageStore/EventLog. These ARE the
33
+ // contract-callable behavioral entry fns.
34
+ // ═══════════════════════════════════════════════════════════════════════════
35
+
36
+ /// `TeamOrchestratorTools` (`tools.py:72-82`). Scope anchored on spawn-time env: no
37
+ /// candidate scan of state/messages/runtime agents (C13-C17). Constructed with the
38
+ /// workspace; `agent_id`/`owner_team_id` are captured from env at construction.
39
+ pub struct TeamOrchestratorTools {
40
+ /// Resolved (`workspace.resolve()`) workspace root.
41
+ workspace: PathBuf,
42
+ /// `TEAM_AGENT_ID` — the sender identity anchor (`None` when absent → `"unknown"`).
43
+ agent_id: Option<AgentId>,
44
+ /// `TEAM_AGENT_OWNER_TEAM_ID` — the scope anchor (`None` → legacy single-team).
45
+ owner_team_id: Option<TeamKey>,
46
+ }
47
+
48
+ impl TeamOrchestratorTools {
49
+ /// `__init__(workspace)` (`tools.py:79-82`): resolve workspace, read identity/scope
50
+ /// from `TEAM_AGENT_ID` / `TEAM_AGENT_OWNER_TEAM_ID` env (`_text` empties → None).
51
+ pub fn new(workspace: &Path) -> Self {
52
+ let agent_id = std::env::var("TEAM_AGENT_ID")
53
+ .ok()
54
+ .and_then(|s| non_empty_string(&s).map(ToString::to_string))
55
+ .map(AgentId::new);
56
+ let owner_team_id = std::env::var("TEAM_AGENT_OWNER_TEAM_ID")
57
+ .ok()
58
+ .and_then(|s| non_empty_string(&s).map(ToString::to_string))
59
+ .map(TeamKey::new);
60
+ Self::with_identity(workspace, agent_id, owner_team_id)
61
+ }
62
+
63
+ /// Test/explicit-injection constructor: bind identity/scope directly instead of
64
+ /// reading env (so contracts can exercise scoped behavior deterministically).
65
+ pub fn with_identity(workspace: &Path, agent_id: Option<AgentId>, owner_team_id: Option<TeamKey>) -> Self {
66
+ Self {
67
+ workspace: std::fs::canonicalize(workspace).unwrap_or_else(|_| workspace.to_path_buf()),
68
+ agent_id,
69
+ owner_team_id,
70
+ }
71
+ }
72
+
73
+ /// `assign_task` (`tools.py:84-133`): C8 Family-B task-view reconcile then deliver.
74
+ /// Resolves team key from owner-team env (or `active_team_key`), appends or
75
+ /// field-updates the task in state, then delegates delivery to
76
+ /// [`Self::send_message`] and compacts the result.
77
+ pub fn assign_task(&self, task: &Value, message: Option<&str>) -> ToolResult {
78
+ let Some(task_obj) = task.as_object() else {
79
+ return Err(ToolError::new(
80
+ ToolErrorReason::InvalidToolArguments,
81
+ "assign_task task must be an object",
82
+ "ValueError",
83
+ ));
84
+ };
85
+ let Some(task_id) = task.get("id").and_then(Value::as_str).and_then(non_empty_string) else {
86
+ return Err(ToolError::new(
87
+ ToolErrorReason::InvalidToolArguments,
88
+ "assign_task task.id is required",
89
+ "ValueError",
90
+ ));
91
+ };
92
+ let Some(assignee) = task
93
+ .get("assignee")
94
+ .and_then(Value::as_str)
95
+ .and_then(non_empty_string)
96
+ else {
97
+ return Err(ToolError::new(
98
+ ToolErrorReason::InvalidToolArguments,
99
+ "assign_task task.assignee is required",
100
+ "ValueError",
101
+ ));
102
+ };
103
+
104
+ let task_value = Value::Object(task_obj.clone());
105
+ let mut state = load_runtime_state(&self.workspace).map_err(tool_runtime_error)?;
106
+ ensure_object(&mut state);
107
+ let team_key = assignment_team_key(&state, self.owner_team_id.as_ref());
108
+ reconcile_assigned_task(&mut state, team_key.as_deref(), &task_value);
109
+ save_runtime_state(&self.workspace, &state).map_err(tool_runtime_error)?;
110
+
111
+ let content = assignment_message(task, message);
112
+ let out = self.send_message(
113
+ &MessageTarget::Single(assignee.to_string()),
114
+ &content,
115
+ Some(task_id),
116
+ None,
117
+ None,
118
+ None,
119
+ )?;
120
+ compact_tool_result(&out.to_value())
121
+ }
122
+
123
+ /// `send_message` (`tools.py:135-183`): C14/C15/C17 scope resolution.
124
+ /// - sender = explicit / `TEAM_AGENT_ID` env / `"unknown"` (no candidate scan).
125
+ /// - `requires_ack` defaults from target (`_requires_ack_for_target`).
126
+ /// - C23 cross-team pre-refusal ([`Self::refuse_cross_team_peer`]) before any
127
+ /// runtime call.
128
+ /// - delegates to [`messaging::send_message`], writes `mcp.scope_resolved`.
129
+ /// - worker recipient + message_id → [`SendOutcome::WorkerAccepted`]; else
130
+ /// [`SendOutcome::Direct`].
131
+ /// Returns `Err(ToolError{PeerNotInScope})` on a refused cross-team peer.
132
+ pub fn send_message(
133
+ &self,
134
+ to: &MessageTarget,
135
+ content: &str,
136
+ task_id: Option<&str>,
137
+ sender: Option<&str>,
138
+ requires_ack: Option<bool>,
139
+ scope: Option<Scope>,
140
+ ) -> Result<SendOutcome, ToolError> {
141
+ if let Some(err) = self.refuse_cross_team_peer(to, scope) {
142
+ return Err(err);
143
+ }
144
+ let sender = sender
145
+ .and_then(non_empty_string)
146
+ .or_else(|| self.agent_id.as_ref().map(AgentId::as_str))
147
+ .unwrap_or("unknown");
148
+ let ack = requires_ack.unwrap_or_else(|| requires_ack_for_target(to));
149
+ // C14/C15/C17 scope audit (#230 I-2/I-6 contract): emit mcp.scope_resolved
150
+ // for every worker-origin send before any routing/delivery — the funnel
151
+ // assertions grep this event to verify the worker call was scoped under the
152
+ // spawn-time owner-team env, not a back-inferred default.
153
+ EventLog::new(&self.workspace)
154
+ .write(
155
+ "mcp.scope_resolved",
156
+ serde_json::json!({
157
+ "tool": "send_message",
158
+ "sender": sender,
159
+ "owner_team_id": self.owner_team_id.as_ref().map(TeamKey::as_str),
160
+ "to": match to {
161
+ MessageTarget::Single(t) => serde_json::Value::String(t.clone()),
162
+ MessageTarget::Broadcast => serde_json::Value::String("*".to_string()),
163
+ MessageTarget::Fanout(list) => serde_json::Value::Array(
164
+ list.iter().map(|s| serde_json::Value::String(s.clone())).collect(),
165
+ ),
166
+ },
167
+ "requires_ack": ack,
168
+ }),
169
+ )
170
+ .map_err(tool_runtime_error)?;
171
+ if is_worker_recipient(to) {
172
+ let recipient = match to {
173
+ MessageTarget::Single(value) => value.as_str(),
174
+ MessageTarget::Broadcast | MessageTarget::Fanout(_) => "worker",
175
+ };
176
+ let store = MessageStore::open(&self.workspace).map_err(tool_runtime_error)?;
177
+ let message_id = store
178
+ .create_message(
179
+ task_id,
180
+ sender,
181
+ recipient,
182
+ content,
183
+ None,
184
+ ack,
185
+ self.owner_team_id.as_ref().map(TeamKey::as_str),
186
+ )
187
+ .map_err(tool_runtime_error)?;
188
+ return Ok(SendOutcome::WorkerAccepted {
189
+ poll_via: format!("team-agent inbox {message_id}"),
190
+ message_id,
191
+ });
192
+ }
193
+ let opts = SendOptions {
194
+ task_id: task_id.map(TaskId::new),
195
+ route_task_id: true,
196
+ sender: sender.to_string(),
197
+ requires_ack: ack,
198
+ team: self.owner_team_id.clone(),
199
+ ..SendOptions::default()
200
+ };
201
+ let out = messaging::send_message(&self.workspace, to, content, &opts).map_err(tool_runtime_error)?;
202
+ let value = delivery_outcome_value(&out);
203
+ let ok = compact_tool_result(&value)?;
204
+ Ok(SendOutcome::Direct(ok))
205
+ }
206
+
207
+ /// `report_result` (`tools.py:249-279`): build & normalize the result envelope
208
+ /// (inferring `task_id`/`agent_id` with byte-stable `"manual"`/`"unknown"`
209
+ /// fallbacks), then delegate to [`messaging::report_result`] and compact.
210
+ #[allow(clippy::too_many_arguments)]
211
+ pub fn report_result(
212
+ &self,
213
+ envelope: Option<&Value>,
214
+ summary: Option<&str>,
215
+ status: ResultStatus,
216
+ changes: Option<&[Value]>,
217
+ tests: Option<&[Value]>,
218
+ risks: Option<&[Value]>,
219
+ artifacts: Option<&[Value]>,
220
+ next_actions: Option<&[Value]>,
221
+ task_id: Option<&str>,
222
+ agent_id: Option<&str>,
223
+ ) -> ToolResult {
224
+ let mut base = envelope.cloned().unwrap_or_else(|| Value::Object(serde_json::Map::new()));
225
+ ensure_object(&mut base);
226
+ if let Some(obj) = base.as_object_mut() {
227
+ if !obj.contains_key("summary") {
228
+ obj.insert(
229
+ "summary".to_string(),
230
+ Value::String(summary.map_or_else(|| "completed".to_string(), ToString::to_string)),
231
+ );
232
+ }
233
+ if !obj.contains_key("status") {
234
+ obj.insert("status".to_string(), enum_value(status));
235
+ }
236
+ if !obj.contains_key("task_id") {
237
+ let resolved = task_id
238
+ .map(ToString::to_string)
239
+ .or_else(|| self.agent_id
240
+ .as_ref()
241
+ .and_then(|agent| latest_task_for_assignee(&self.workspace, agent.as_str())))
242
+ .unwrap_or_else(|| "manual".to_string());
243
+ obj.insert("task_id".to_string(), Value::String(resolved));
244
+ }
245
+ if !obj.contains_key("agent_id") {
246
+ let resolved = agent_id
247
+ .map(ToString::to_string)
248
+ .or_else(|| self.agent_id.as_ref().map(|env_agent| env_agent.as_str().to_string()))
249
+ .unwrap_or_else(|| "unknown".to_string());
250
+ obj.insert("agent_id".to_string(), Value::String(resolved));
251
+ }
252
+ if !obj.contains_key("changes") {
253
+ insert_array(obj, "changes", changes);
254
+ }
255
+ if !obj.contains_key("tests") {
256
+ insert_array(obj, "tests", tests);
257
+ }
258
+ if !obj.contains_key("risks") {
259
+ insert_array(obj, "risks", risks);
260
+ }
261
+ if !obj.contains_key("artifacts") {
262
+ insert_array(obj, "artifacts", artifacts);
263
+ }
264
+ if !obj.contains_key("next_actions") {
265
+ insert_array(obj, "next_actions", next_actions);
266
+ }
267
+ }
268
+ let normalized = normalize_report_envelope(&base);
269
+ let env_value = normalized_envelope_value(&normalized);
270
+ messaging::report_result(&self.workspace, &env_value)
271
+ .map_err(tool_runtime_error)
272
+ .and_then(|value| compact_tool_result(&value))
273
+ }
274
+
275
+ /// `update_state` (`tools.py:316-325`): append a note to `state.notes`, save, then
276
+ /// rewrite `team_state.md` (delegated to step 13 [`write_team_state`]). Returns
277
+ /// `{ok:true, state_file:<path>}`.
278
+ ///
279
+ /// [`write_team_state`]: super::lifecycle_placeholder::write_team_state
280
+ pub fn update_state(&self, note: &str) -> ToolResult {
281
+ let mut state = load_runtime_state(&self.workspace).map_err(tool_runtime_error)?;
282
+ ensure_object(&mut state);
283
+ if let Some(obj) = state.as_object_mut() {
284
+ let notes = obj
285
+ .entry("notes".to_string())
286
+ .or_insert_with(|| Value::Array(Vec::new()));
287
+ if !notes.is_array() {
288
+ *notes = Value::Array(Vec::new());
289
+ }
290
+ if let Some(items) = notes.as_array_mut() {
291
+ items.push(Value::String(note.to_string()));
292
+ }
293
+ }
294
+ save_runtime_state(&self.workspace, &state).map_err(tool_runtime_error)?;
295
+ let path = super::lifecycle_placeholder::write_team_state(&self.workspace, &Value::Null, &state)
296
+ .map_err(tool_runtime_error)?;
297
+ let mut fields = serde_json::Map::new();
298
+ fields.insert("ok".to_string(), Value::Bool(true));
299
+ fields.insert("state_file".to_string(), Value::String(path.to_string_lossy().to_string()));
300
+ Ok(ToolOk { fields })
301
+ }
302
+
303
+ /// `get_team_status` (`tools.py:327-328`): machine-readable status —
304
+ /// `runtime.status(workspace, as_json=true, compact=true)` (delegated to step 13
305
+ /// [`runtime_status`]). Returns the compact status object verbatim.
306
+ ///
307
+ /// [`runtime_status`]: super::lifecycle_placeholder::runtime_status
308
+ pub fn get_team_status(&self) -> ToolResult {
309
+ match super::lifecycle_placeholder::runtime_status(&self.workspace, true) {
310
+ Ok(value) => Ok(ToolOk { fields: object_fields(value) }),
311
+ Err(err) => Err(tool_runtime_error(err)),
312
+ }
313
+ }
314
+
315
+ /// `stop_agent` (`tools.py:330-331`): delegate to step 13 [`stop_agent`], compact.
316
+ ///
317
+ /// [`stop_agent`]: super::lifecycle_placeholder::stop_agent
318
+ pub fn stop_agent(&self, agent_id: &str) -> ToolResult {
319
+ super::lifecycle_placeholder::stop_agent(&self.workspace, agent_id)
320
+ .map_err(tool_runtime_error)
321
+ .and_then(|v| compact_tool_result(&v))
322
+ }
323
+
324
+ /// `reset_agent` (`tools.py:333-334`): delegate to step 13 [`reset_agent`]
325
+ /// (`discard_session`), compact.
326
+ ///
327
+ /// [`reset_agent`]: super::lifecycle_placeholder::reset_agent
328
+ pub fn reset_agent(&self, agent_id: &str, discard_session: bool) -> ToolResult {
329
+ super::lifecycle_placeholder::reset_agent(&self.workspace, agent_id, discard_session)
330
+ .map_err(tool_runtime_error)
331
+ .and_then(|v| compact_tool_result(&v))
332
+ }
333
+
334
+ /// `add_agent` (`tools.py:336-337`): delegate to step 13 [`add_agent`]
335
+ /// (workspace-relative role file), compact.
336
+ ///
337
+ /// [`add_agent`]: super::lifecycle_placeholder::add_agent
338
+ pub fn add_agent(&self, new_agent_id: &str, role_file_path: &str) -> ToolResult {
339
+ super::lifecycle_placeholder::add_agent(&self.workspace, new_agent_id, role_file_path)
340
+ .map_err(tool_runtime_error)
341
+ .and_then(|v| compact_tool_result(&v))
342
+ }
343
+
344
+ /// `fork_agent` (`tools.py:339-340`): delegate to step 13 [`fork_agent`], compact.
345
+ ///
346
+ /// [`fork_agent`]: super::lifecycle_placeholder::fork_agent
347
+ pub fn fork_agent(&self, source_agent_id: &str, as_agent_id: &str, label: Option<&str>) -> ToolResult {
348
+ super::lifecycle_placeholder::fork_agent(&self.workspace, source_agent_id, as_agent_id, label)
349
+ .map_err(tool_runtime_error)
350
+ .and_then(|v| compact_tool_result(&v))
351
+ }
352
+
353
+ /// `request_human` (`tools.py:342-346`): create a `requires_ack` leader message via
354
+ /// [`MessageStore::create_message`]; sender = env / inferred / `"unknown"`. Returns
355
+ /// `{ok:true, message_id, status:"needs_human"}`.
356
+ pub fn request_human(&self, question: &str, task_id: Option<&str>, agent_id: Option<&str>) -> ToolResult {
357
+ let explicit_sender = agent_id.and_then(non_empty_string);
358
+ let sender = explicit_sender
359
+ .or_else(|| self.agent_id.as_ref().map(AgentId::as_str))
360
+ .unwrap_or("unknown");
361
+ let event_log = EventLog::new(&self.workspace);
362
+ if explicit_sender.is_none() && self.agent_id.is_none() {
363
+ event_log
364
+ .write(
365
+ "mcp.identity_inference_failed",
366
+ serde_json::json!({"tool": "request_human"}),
367
+ )
368
+ .map_err(tool_runtime_error)?;
369
+ }
370
+ // #230 N31/N32 funnel: request_human is a leader-bound caller and must go through
371
+ // the same primitive as send_message(to=leader) / report_result / idle reminder.
372
+ // The legacy path was a raw `store.create_message(... recipient="leader" ...)` that
373
+ // bypassed the leader-delivery audit (no deliver_to_leader.submit emit, no rebind
374
+ // guard, no leader_notification_log dedup). funnel it now.
375
+ let state = crate::state::persist::load_runtime_state(&self.workspace)
376
+ .unwrap_or(serde_json::json!({}));
377
+ let task = task_id.map(|t| TaskId::new(t.to_string()));
378
+ let outcome = crate::messaging::send_to_leader_receiver(
379
+ &self.workspace,
380
+ &state,
381
+ "leader",
382
+ question,
383
+ task.as_ref(),
384
+ sender,
385
+ true,
386
+ None,
387
+ &event_log,
388
+ )
389
+ .map_err(tool_runtime_error)?;
390
+ let mut fields = serde_json::Map::new();
391
+ fields.insert("ok".to_string(), Value::Bool(outcome.ok));
392
+ fields.insert(
393
+ "message_id".to_string(),
394
+ outcome.message_id.clone().map_or(Value::Null, Value::String),
395
+ );
396
+ fields.insert("status".to_string(), Value::String("needs_human".to_string()));
397
+ Ok(ToolOk { fields })
398
+ }
399
+
400
+ /// `stuck_list` (`tools.py:348-349`): delegate to [`messaging::stuck_list`] (the
401
+ /// team-scoped suppressed-alert projection).
402
+ pub fn stuck_list(&self) -> ToolResult {
403
+ messaging::stuck_list(&self.workspace)
404
+ .map_err(tool_runtime_error)
405
+ .map(|v| ToolOk { fields: object_fields(v) })
406
+ }
407
+
408
+ /// `stuck_cancel` (`tools.py:351-352`): delegate to [`messaging::stuck_cancel`];
409
+ /// `suppressed_by` = env agent_id / `"leader"`.
410
+ pub fn stuck_cancel(&self, agent_id: &str, alert_type: &str) -> ToolResult {
411
+ let alert = match alert_type {
412
+ "stuck" => Some(messaging::AlertType::Stuck),
413
+ "idle_fallback" => Some(messaging::AlertType::IdleFallback),
414
+ "cross_worker_deadlock" => Some(messaging::AlertType::CrossWorkerDeadlock),
415
+ "all" => None,
416
+ _ => None,
417
+ };
418
+ let suppressed_by = self.agent_id.as_ref().map(AgentId::as_str).unwrap_or("leader");
419
+ messaging::stuck_cancel(&self.workspace, agent_id, alert, suppressed_by)
420
+ .map_err(tool_runtime_error)
421
+ .map(|v| ToolOk { fields: object_fields(v) })
422
+ }
423
+
424
+ /// `get_visible_peers` (`tools.py:226-247`): C16 scope-filtered peer list — live
425
+ /// agents within the spawn-time owner-team scope only; other teams and dead/stopped
426
+ /// agents are filtered server-side and never named.
427
+ pub fn get_visible_peers(&self) -> Result<VisiblePeers, McpError> {
428
+ let mut peers = Vec::new();
429
+ if let Some(team) = &self.owner_team_id {
430
+ let state = load_runtime_state(&self.workspace)?;
431
+ if let Some(agents) = state
432
+ .get("teams")
433
+ .and_then(|v| v.get(team.as_str()))
434
+ .and_then(|v| v.get("agents"))
435
+ .and_then(Value::as_object)
436
+ {
437
+ for (agent_id, info) in agents {
438
+ let status = info
439
+ .as_object()
440
+ .and_then(|obj| obj.get("status"))
441
+ .and_then(Value::as_str)
442
+ .unwrap_or("")
443
+ .to_ascii_lowercase();
444
+ if status == "dead" || status == "stopped" {
445
+ continue;
446
+ }
447
+ peers.push(AgentId::new(agent_id.clone()));
448
+ }
449
+ }
450
+ }
451
+ peers.sort_by(|a, b| a.as_str().cmp(b.as_str()));
452
+ Ok(VisiblePeers {
453
+ peers,
454
+ sender_team_id: self.owner_team_id.clone(),
455
+ scope: Scope::Team,
456
+ })
457
+ }
458
+
459
+ /// `_refuse_cross_team_peer` (`tools.py:185-213`): server-side C23 pre-refusal. A
460
+ /// non-`*`/non-leader string target NOT in the visible-peer scope and not the
461
+ /// sender itself, with `scope != workspace`, → `Some(ToolError{PeerNotInScope})`
462
+ /// (also writes `mcp.send_message_refused`). `None` = allowed to proceed.
463
+ pub fn refuse_cross_team_peer(&self, to: &MessageTarget, scope: Option<Scope>) -> Option<ToolError> {
464
+ if scope == Some(Scope::Workspace) || self.owner_team_id.is_none() {
465
+ return None;
466
+ }
467
+ let MessageTarget::Single(target) = to else {
468
+ return None;
469
+ };
470
+ if target.is_empty()
471
+ || target == "*"
472
+ || target == "leader"
473
+ || target == "Leader"
474
+ || self.agent_id.as_ref().is_some_and(|id| id.as_str() == target)
475
+ {
476
+ return None;
477
+ }
478
+ if let Ok(visible) = self.get_visible_peers() {
479
+ if visible.peers.iter().any(|peer| peer.as_str() == target) {
480
+ return None;
481
+ }
482
+ }
483
+ let hint = "the requested peer is not part of your team. pass scope='workspace' to address peers in other teams.";
484
+ let _ = EventLog::new(&self.workspace).write(
485
+ "mcp.send_message_refused",
486
+ serde_json::json!({
487
+ "reason": "peer_not_in_scope",
488
+ "sender_team_id": self.owner_team_id.as_ref().map(TeamKey::as_str).unwrap_or(""),
489
+ "scope": "team",
490
+ "hint": hint
491
+ }),
492
+ );
493
+ let mut extra = serde_json::Map::new();
494
+ extra.insert("status".to_string(), Value::String("refused".to_string()));
495
+ extra.insert(
496
+ "hint".to_string(),
497
+ Value::String(hint.to_string()),
498
+ );
499
+ Some(ToolError {
500
+ reason: ToolErrorReason::PeerNotInScope,
501
+ exc_type: "PeerNotInScope".to_string(),
502
+ message: format!("peer '{target}' is not in scope"),
503
+ extra,
504
+ })
505
+ }
506
+ }
507
+
508
+ fn assignment_team_key(state: &Value, owner_team_id: Option<&TeamKey>) -> Option<String> {
509
+ owner_team_id
510
+ .map(|team| team.as_str().to_string())
511
+ .or_else(|| {
512
+ state
513
+ .get("active_team_key")
514
+ .and_then(Value::as_str)
515
+ .and_then(non_empty_string)
516
+ .map(ToString::to_string)
517
+ })
518
+ }
519
+
520
+ fn reconcile_assigned_task(state: &mut Value, team_key: Option<&str>, task: &Value) {
521
+ let mut top = state
522
+ .get("tasks")
523
+ .and_then(Value::as_array)
524
+ .cloned()
525
+ .unwrap_or_default();
526
+ upsert_task_in_place(&mut top, task);
527
+ if let Some(root) = state.as_object_mut() {
528
+ root.insert("tasks".to_string(), Value::Array(top.clone()));
529
+ }
530
+ if let Some(key) = team_key {
531
+ let mut team_tasks = state
532
+ .get("teams")
533
+ .and_then(|v| v.get(key))
534
+ .and_then(|team| team.get("tasks"))
535
+ .and_then(Value::as_array)
536
+ .cloned()
537
+ .unwrap_or_default();
538
+ upsert_task_in_place(&mut team_tasks, task);
539
+ write_team_tasks(state, key, team_tasks);
540
+ }
541
+ }
542
+
543
+ fn upsert_task_in_place(tasks: &mut Vec<Value>, task: &Value) {
544
+ let Some(task_id) = task.get("id").and_then(Value::as_str) else {
545
+ return;
546
+ };
547
+ for existing in tasks.iter_mut() {
548
+ if existing.get("id").and_then(Value::as_str) == Some(task_id) {
549
+ merge_object_fields(existing, task);
550
+ return;
551
+ }
552
+ }
553
+ tasks.push(task.clone());
554
+ }
555
+
556
+ fn merge_object_fields(existing: &mut Value, incoming: &Value) {
557
+ let Some(existing_obj) = existing.as_object_mut() else {
558
+ *existing = incoming.clone();
559
+ return;
560
+ };
561
+ let Some(incoming_obj) = incoming.as_object() else {
562
+ return;
563
+ };
564
+ for (key, value) in incoming_obj {
565
+ existing_obj.insert(key.clone(), value.clone());
566
+ }
567
+ }
568
+
569
+ fn write_team_tasks(state: &mut Value, team_key: &str, tasks: Vec<Value>) {
570
+ let Some(root) = state.as_object_mut() else {
571
+ return;
572
+ };
573
+ let teams = root
574
+ .entry("teams".to_string())
575
+ .or_insert_with(|| Value::Object(serde_json::Map::new()));
576
+ let Some(teams_obj) = teams.as_object_mut() else {
577
+ return;
578
+ };
579
+ let team = teams_obj
580
+ .entry(team_key.to_string())
581
+ .or_insert_with(|| {
582
+ let mut team = serde_json::Map::new();
583
+ team.insert("tasks".to_string(), Value::Array(Vec::new()));
584
+ team.insert("status".to_string(), Value::String("alive".to_string()));
585
+ Value::Object(team)
586
+ });
587
+ let Some(team_obj) = team.as_object_mut() else {
588
+ return;
589
+ };
590
+ team_obj.insert("tasks".to_string(), Value::Array(tasks));
591
+ }
592
+
593
+ fn assignment_message(task: &Value, explicit: Option<&str>) -> String {
594
+ if let Some(message) = explicit.and_then(non_empty_string) {
595
+ return message.to_string();
596
+ }
597
+ for key in ["description", "title"] {
598
+ if let Some(text) = task.get(key).and_then(Value::as_str).and_then(non_empty_string) {
599
+ return text.to_string();
600
+ }
601
+ }
602
+ json_dumps_default(task)
603
+ }