@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,680 @@
1
+ //! spec / result-envelope 校验(`spec.py`)。
2
+ //!
3
+ //! 本 module **操作解析后的 Value**(非 typed struct)——与 Python 一样校验 dict,
4
+ //! 产出**逐字节一致的有序错误消息列表**(测试对 Python 真相源取 golden 锁死)。
5
+ //! serde_json `preserve_order` 保证 child 迭代序 == Python 插入序。
6
+ //!
7
+ //! 本文件先落地 `validate_result_envelope`(自包含,step 7/11 的门)。`validate_spec`
8
+ //! 依赖 `expand_tools`(permissions)+ `find_dependency_cycle`(task_graph)+ yaml Value,
9
+ //! 待那几个叶子模块集成后再加。
10
+
11
+ use std::path::Path;
12
+
13
+ use serde_json::Value;
14
+
15
+ use crate::model::enums::TaskStatus;
16
+ use crate::model::errors::ModelError;
17
+ use crate::model::ids::TaskId;
18
+ use crate::model::task_graph::{find_dependency_cycle, TaskNode};
19
+ use crate::model::yaml::Value as Yaml;
20
+ use crate::model::{permissions, yaml};
21
+
22
+ /// result_envelope_v1 顶层 required(= allowed)。
23
+ const RESULT_REQUIRED: &[&str] = &[
24
+ "schema_version",
25
+ "task_id",
26
+ "agent_id",
27
+ "status",
28
+ "summary",
29
+ "changes",
30
+ "tests",
31
+ "risks",
32
+ "artifacts",
33
+ "next_actions",
34
+ ];
35
+
36
+ /// `RESULT_COLLECTION_SCHEMAS`(`spec.py:93-99`),有序:(field, required, allowed)。
37
+ const RESULT_COLLECTIONS: &[(&str, &[&str], &[&str])] = &[
38
+ ("changes", &["path", "kind", "description"], &["path", "kind", "description"]),
39
+ ("tests", &["command", "status"], &["command", "status", "detail"]),
40
+ ("risks", &["severity", "description"], &["severity", "description"]),
41
+ ("artifacts", &["path", "description"], &["path", "description"]),
42
+ ("next_actions", &["description"], &["description"]),
43
+ ];
44
+
45
+ /// `spec.validate_result_envelope`:校验 result_envelope_v1。失败 → `ModelError::Validation`,
46
+ /// 消息体与 Python 字节一致(`result_envelope_v1 validation failed:\n- ...`)。
47
+ pub fn validate_result_envelope(envelope: &Value) -> Result<(), ModelError> {
48
+ let errors = result_schema_errors(envelope);
49
+ if errors.is_empty() {
50
+ return Ok(());
51
+ }
52
+ let joined = errors
53
+ .iter()
54
+ .map(|e| format!("- {e}"))
55
+ .collect::<Vec<_>>()
56
+ .join("\n");
57
+ Err(ModelError::Validation(format!(
58
+ "result_envelope_v1 validation failed:\n{joined}"
59
+ )))
60
+ }
61
+
62
+ /// `spec._check_keys`:非 object → "must be an object";否则 missing(排序)+ unknown(排序)。
63
+ fn check_keys(obj: &Value, path: &str, required: &[&str], allowed: &[&str], errors: &mut Vec<String>) {
64
+ let Some(map) = obj.as_object() else {
65
+ errors.push(format!("{path}: must be an object"));
66
+ return;
67
+ };
68
+ let base = path.trim_end_matches('/');
69
+ let mut missing: Vec<&str> = required
70
+ .iter()
71
+ .copied()
72
+ .filter(|k| !map.contains_key(*k))
73
+ .collect();
74
+ missing.sort_unstable();
75
+ for k in missing {
76
+ errors.push(format!("{base}/{k}: missing required field"));
77
+ }
78
+ let mut unknown: Vec<&str> = map
79
+ .keys()
80
+ .map(String::as_str)
81
+ .filter(|k| !allowed.contains(k))
82
+ .collect();
83
+ unknown.sort_unstable();
84
+ for k in unknown {
85
+ errors.push(format!("{base}/{k}: unknown field"));
86
+ }
87
+ }
88
+
89
+ /// `spec._result_schema_errors`,逐行对齐(含错误产出顺序)。
90
+ fn result_schema_errors(envelope: &Value) -> Vec<String> {
91
+ let mut errors = Vec::new();
92
+ check_keys(envelope, "/", RESULT_REQUIRED, RESULT_REQUIRED, &mut errors);
93
+ let Some(map) = envelope.as_object() else {
94
+ return errors;
95
+ };
96
+
97
+ if map.get("schema_version").and_then(Value::as_str) != Some("result_envelope_v1") {
98
+ errors.push("/schema_version: must be result_envelope_v1".to_string());
99
+ }
100
+
101
+ for field in ["task_id", "agent_id", "summary"] {
102
+ if let Some(v) = map.get(field) {
103
+ if !v.is_string() {
104
+ errors.push(format!("/{field}: must be a string"));
105
+ } else if v.as_str() == Some("") {
106
+ errors.push(format!("/{field}: must not be empty"));
107
+ }
108
+ }
109
+ }
110
+
111
+ if !matches!(
112
+ map.get("status").and_then(Value::as_str),
113
+ Some("success" | "blocked" | "failed" | "partial")
114
+ ) {
115
+ errors.push("/status: invalid result status".to_string());
116
+ }
117
+
118
+ if map.contains_key("schema") {
119
+ errors.push("/schema: use schema_version, not schema".to_string());
120
+ }
121
+
122
+ for (field, item_required, item_allowed) in RESULT_COLLECTIONS {
123
+ let Some(value) = map.get(*field) else {
124
+ continue;
125
+ };
126
+ let Some(arr) = value.as_array() else {
127
+ errors.push(format!("/{field}: must be a list"));
128
+ continue;
129
+ };
130
+ for (idx, item) in arr.iter().enumerate() {
131
+ let item_path = format!("/{field}/{idx}");
132
+ check_keys(item, &item_path, item_required, item_allowed, &mut errors);
133
+ let Some(item_map) = item.as_object() else {
134
+ continue;
135
+ };
136
+ // 枚举字段校验(changes.kind / tests.status / risks.severity):值不在合法集 → 报错。
137
+ let enum_field: Option<(&str, &[&str], &str)> = match *field {
138
+ "changes" => Some((
139
+ "kind",
140
+ &["created", "modified", "deleted", "observed"],
141
+ "invalid change kind",
142
+ )),
143
+ "tests" => Some((
144
+ "status",
145
+ &["passed", "failed", "not_run", "skipped"],
146
+ "invalid test status",
147
+ )),
148
+ "risks" => Some(("severity", &["low", "medium", "high"], "invalid risk severity")),
149
+ _ => None,
150
+ };
151
+ if let Some((key, valid, msg)) = enum_field {
152
+ let value_ok = item_map
153
+ .get(key)
154
+ .and_then(Value::as_str)
155
+ .is_some_and(|s| valid.contains(&s));
156
+ if !value_ok {
157
+ errors.push(format!("{item_path}/{key}: {msg}"));
158
+ }
159
+ }
160
+ // child string 校验:插入序(preserve_order)== Python item.items()。
161
+ for (key, child) in item_map {
162
+ if item_allowed.contains(&key.as_str()) && !child.is_string() {
163
+ errors.push(format!("{item_path}/{key}: must be a string"));
164
+ }
165
+ }
166
+ }
167
+ }
168
+ errors
169
+ }
170
+
171
+ // ===================== validate_spec(team.spec.yaml) =====================
172
+ // 操作 yaml::Value(spec 走 simple_yaml)。basic_schema 全部先于 semantic;错误消息逐字节
173
+ // 对齐 Python `spec.validate_spec`(golden 由真相源双跑锁死)。
174
+
175
+ const ROOT_KEYS: &[&str] = &[
176
+ "version", "team", "leader", "agents", "routing", "communication", "runtime", "context", "tasks",
177
+ ];
178
+ const SUPPORTED_PROVIDERS: &[&str] = &["claude", "claude_code", "codex", "gemini_cli", "fake"];
179
+ const AUTH_MODES: &[&str] = &["subscription", "official_api", "compatible_api"];
180
+ const VALID_DISPLAY_BACKENDS: &[&str] = &[
181
+ "none", "tmux_attach", "iterm", "ghostty", "ghostty_window", "ghostty_workspace", "adaptive",
182
+ ];
183
+ const TASK_STATUS_STRS: &[&str] = &[
184
+ "pending", "ready", "running", "blocked", "needs_retry", "done", "failed", "cancelled",
185
+ ];
186
+
187
+ /// `spec.validate_spec`:basic schema + semantic 校验。失败 → `ModelError::Validation`,
188
+ /// 消息与 Python 字节一致(`team.spec.yaml validation failed:\n- ...`)。
189
+ pub fn validate_spec(spec: &Yaml, base_dir: &Path) -> Result<(), ModelError> {
190
+ let mut errors = basic_schema_errors(spec);
191
+ errors.extend(semantic_errors(spec, base_dir));
192
+ if errors.is_empty() {
193
+ return Ok(());
194
+ }
195
+ let joined = errors.iter().map(|m| format!("- {m}")).collect::<Vec<_>>().join("\n");
196
+ Err(ModelError::Validation(format!(
197
+ "team.spec.yaml validation failed:\n{joined}"
198
+ )))
199
+ }
200
+
201
+ /// 便捷:YAML 文本 → load → 校验(对应 `load_spec` 的校验部分,不含 deprecation 发射)。
202
+ pub fn load_and_validate_spec(text: &str, base_dir: &Path) -> Result<Yaml, ModelError> {
203
+ let spec = yaml::loads(text)?;
204
+ validate_spec(&spec, base_dir)?;
205
+ Ok(spec)
206
+ }
207
+
208
+ fn is_map_some(v: Option<&Yaml>) -> bool {
209
+ v.is_some_and(Yaml::is_map)
210
+ }
211
+
212
+ /// `spec._check_keys` 的 yaml::Value 版。
213
+ fn check_keys_y(obj: Option<&Yaml>, path: &str, required: &[&str], allowed: &[&str], errors: &mut Vec<String>) {
214
+ let Some(map) = obj.and_then(Yaml::as_map) else {
215
+ errors.push(format!("{path}: must be an object"));
216
+ return;
217
+ };
218
+ let base = path.trim_end_matches('/');
219
+ let mut missing: Vec<&str> = required
220
+ .iter()
221
+ .copied()
222
+ .filter(|k| !map.iter().any(|(mk, _)| mk == k))
223
+ .collect();
224
+ missing.sort_unstable();
225
+ for k in missing {
226
+ errors.push(format!("{base}/{k}: missing required field"));
227
+ }
228
+ let mut unknown: Vec<&str> = map
229
+ .iter()
230
+ .map(|(k, _)| k.as_str())
231
+ .filter(|k| !allowed.contains(k))
232
+ .collect();
233
+ unknown.sort_unstable();
234
+ for k in unknown {
235
+ errors.push(format!("{base}/{k}: unknown field"));
236
+ }
237
+ }
238
+
239
+ fn check_list_y(value: Option<&Yaml>, path: &str, errors: &mut Vec<String>) {
240
+ if !matches!(value, Some(Yaml::List(_))) {
241
+ errors.push(format!("{path}: must be a list"));
242
+ }
243
+ }
244
+
245
+ fn basic_schema_errors(spec: &Yaml) -> Vec<String> {
246
+ let mut e = Vec::new();
247
+ check_keys_y(Some(spec), "/", ROOT_KEYS, ROOT_KEYS, &mut e);
248
+ if !matches!(spec.get("version"), Some(Yaml::Int(1))) {
249
+ e.push("/version: must equal 1".to_string());
250
+ }
251
+ let team_keys = &["name", "mode", "objective", "workspace"];
252
+ check_keys_y(spec.get("team"), "/team", team_keys, team_keys, &mut e);
253
+ let mode = spec.get("team").and_then(|t| t.get("mode")).and_then(Yaml::as_str);
254
+ if !matches!(mode, Some("supervisor_worker" | "swarm_limited")) {
255
+ e.push("/team/mode: invalid mode".to_string());
256
+ }
257
+ let leader_keys = &["id", "role", "provider", "model", "tools", "context_policy"];
258
+ check_keys_y(spec.get("leader"), "/leader", leader_keys, leader_keys, &mut e);
259
+ let cp_keys = &["keep_user_thread", "receive_worker_outputs", "max_worker_result_tokens"];
260
+ check_keys_y(
261
+ spec.get("leader").and_then(|l| l.get("context_policy")),
262
+ "/leader/context_policy",
263
+ cp_keys,
264
+ cp_keys,
265
+ &mut e,
266
+ );
267
+ match spec.get("agents") {
268
+ Some(Yaml::List(agents)) if !agents.is_empty() => {
269
+ for (idx, agent) in agents.iter().enumerate() {
270
+ check_agent(agent, &format!("/agents/{idx}"), &mut e);
271
+ }
272
+ }
273
+ _ => e.push("/agents: must be a non-empty list".to_string()),
274
+ }
275
+ check_routing(spec.get("routing"), &mut e);
276
+ check_communication(spec.get("communication"), &mut e);
277
+ check_runtime(spec.get("runtime"), &mut e);
278
+ check_context(spec.get("context"), &mut e);
279
+ match spec.get("tasks") {
280
+ Some(Yaml::List(tasks)) => {
281
+ for (idx, task) in tasks.iter().enumerate() {
282
+ check_task(task, &format!("/tasks/{idx}"), &mut e);
283
+ }
284
+ }
285
+ _ => e.push("/tasks: must be a list".to_string()),
286
+ }
287
+ e
288
+ }
289
+
290
+ fn check_agent(agent: &Yaml, path: &str, errors: &mut Vec<String>) {
291
+ let req = &[
292
+ "id", "role", "provider", "model", "working_directory", "system_prompt", "tools",
293
+ "permission_mode", "preferred_for", "avoid_for", "output_contract",
294
+ ];
295
+ let allowed = &[
296
+ "id", "role", "provider", "model", "working_directory", "system_prompt", "tools",
297
+ "permission_mode", "preferred_for", "avoid_for", "output_contract", "paused", "auth_mode",
298
+ "profile", "credential_ref", "forked_from",
299
+ ];
300
+ check_keys_y(Some(agent), path, req, allowed, errors);
301
+ if !agent.is_map() {
302
+ return;
303
+ }
304
+ check_keys_y(agent.get("system_prompt"), &format!("{path}/system_prompt"), &["inline", "file"], &["inline", "file"], errors);
305
+ check_list_y(agent.get("tools"), &format!("{path}/tools"), errors);
306
+ check_list_y(agent.get("preferred_for"), &format!("{path}/preferred_for"), errors);
307
+ check_list_y(agent.get("avoid_for"), &format!("{path}/avoid_for"), errors);
308
+ check_keys_y(agent.get("output_contract"), &format!("{path}/output_contract"), &["format", "required_fields"], &["format", "required_fields"], errors);
309
+ if agent.get("output_contract").and_then(|o| o.get("format")).and_then(Yaml::as_str) != Some("result_envelope_v1") {
310
+ errors.push(format!("{path}/output_contract/format: must be result_envelope_v1"));
311
+ }
312
+ }
313
+
314
+ fn check_routing(routing: Option<&Yaml>, errors: &mut Vec<String>) {
315
+ check_keys_y(routing, "/routing", &["default_assignee", "rules"], &["default_assignee", "rules"], errors);
316
+ if !is_map_some(routing) {
317
+ return;
318
+ }
319
+ let Some(Yaml::List(rules)) = routing.and_then(|r| r.get("rules")) else {
320
+ errors.push("/routing/rules: must be a list".to_string());
321
+ return;
322
+ };
323
+ for (idx, rule) in rules.iter().enumerate() {
324
+ check_keys_y(Some(rule), &format!("/routing/rules/{idx}"), &["id", "assign_to", "priority"], &["id", "when", "match", "assign_to", "priority"], errors);
325
+ let has_clause = rule.get("when").is_some_and(Yaml::is_truthy) || rule.get("match").is_some_and(Yaml::is_truthy);
326
+ if rule.is_map() && !has_clause {
327
+ errors.push(format!("/routing/rules/{idx}: must include when or match"));
328
+ }
329
+ }
330
+ }
331
+
332
+ fn check_communication(comm: Option<&Yaml>, errors: &mut Vec<String>) {
333
+ let req = &["protocol", "topology", "worker_to_worker", "ack_timeout_sec", "result_format", "message_store"];
334
+ check_keys_y(comm, "/communication", req, req, errors);
335
+ if !is_map_some(comm) {
336
+ return;
337
+ }
338
+ if !matches!(comm.and_then(|c| c.get("protocol")).and_then(Yaml::as_str), Some("mcp_inbox" | "file_bus")) {
339
+ errors.push("/communication/protocol: invalid protocol".to_string());
340
+ }
341
+ if comm.and_then(|c| c.get("result_format")).and_then(Yaml::as_str) != Some("result_envelope_v1") {
342
+ errors.push("/communication/result_format: must be result_envelope_v1".to_string());
343
+ }
344
+ check_keys_y(comm.and_then(|c| c.get("message_store")), "/communication/message_store", &["sqlite", "mirror_files"], &["sqlite", "mirror_files"], errors);
345
+ }
346
+
347
+ fn check_runtime(runtime: Option<&Yaml>, errors: &mut Vec<String>) {
348
+ let req = &["backend", "session_name", "auto_launch", "require_user_approval_before_launch", "max_active_agents", "startup_order"];
349
+ let allowed = &[
350
+ "backend", "session_name", "auto_launch", "require_user_approval_before_launch",
351
+ "max_active_agents", "startup_order", "display_backend", "dangerous_auto_approve",
352
+ "auto_attach_leader", "fast", "tick_interval_sec", "push_min_interval_sec",
353
+ "stuck_timeout_sec", "auto_trust_own_workspace",
354
+ ];
355
+ check_keys_y(runtime, "/runtime", req, allowed, errors);
356
+ if !is_map_some(runtime) {
357
+ return;
358
+ }
359
+ let get = |k: &str| runtime.and_then(|r| r.get(k));
360
+ if !matches!(get("backend").and_then(Yaml::as_str), Some("tmux" | "pty")) {
361
+ errors.push("/runtime/backend: invalid backend".to_string());
362
+ }
363
+ if let Some(db) = get("display_backend") {
364
+ if !db.as_str().is_some_and(|s| VALID_DISPLAY_BACKENDS.contains(&s)) {
365
+ errors.push("/runtime/display_backend: invalid display backend".to_string());
366
+ }
367
+ }
368
+ if get("dangerous_auto_approve").is_some_and(|v| !matches!(v, Yaml::Bool(_))) {
369
+ errors.push("/runtime/dangerous_auto_approve: must be a boolean".to_string());
370
+ }
371
+ if get("auto_trust_own_workspace").is_some_and(|v| !matches!(v, Yaml::Bool(_))) {
372
+ errors.push("/runtime/auto_trust_own_workspace: must be a boolean".to_string());
373
+ }
374
+ check_list_y(get("startup_order"), "/runtime/startup_order", errors);
375
+ }
376
+
377
+ fn check_context(context: Option<&Yaml>, errors: &mut Vec<String>) {
378
+ let req = &["state_file", "artifact_dir", "log_dir", "summarization"];
379
+ check_keys_y(context, "/context", req, req, errors);
380
+ if is_map_some(context) {
381
+ check_keys_y(context.and_then(|c| c.get("summarization")), "/context/summarization", &["worker_full_logs", "state_update"], &["worker_full_logs", "state_update"], errors);
382
+ }
383
+ }
384
+
385
+ fn check_task(task: &Yaml, path: &str, errors: &mut Vec<String>) {
386
+ let req = &["id", "title", "type", "assignee", "deps", "acceptance", "status"];
387
+ let allowed = &[
388
+ "id", "title", "type", "assignee", "deps", "acceptance", "status", "description",
389
+ "requires_tools", "files", "risk", "retry_limit", "human_confirmation",
390
+ ];
391
+ check_keys_y(Some(task), path, req, allowed, errors);
392
+ if !task.is_map() {
393
+ return;
394
+ }
395
+ check_list_y(task.get("deps"), &format!("{path}/deps"), errors);
396
+ check_list_y(task.get("acceptance"), &format!("{path}/acceptance"), errors);
397
+ if !task.get("status").and_then(Yaml::as_str).is_some_and(|s| TASK_STATUS_STRS.contains(&s)) {
398
+ errors.push(format!("{path}/status: invalid task status"));
399
+ }
400
+ }
401
+
402
+ fn semantic_errors(spec: &Yaml, base_dir: &Path) -> Vec<String> {
403
+ use std::collections::HashSet;
404
+ let mut e = Vec::new();
405
+ let leader = spec.get("leader");
406
+ let agents: &[Yaml] = spec.get("agents").and_then(Yaml::as_list).unwrap_or(&[]);
407
+ let map_agents: Vec<&Yaml> = agents.iter().filter(|a| a.is_map()).collect();
408
+
409
+ // duplicate agent id(集合含 None 复刻 Python `{a.get("id") ...}`)。
410
+ let id_set: HashSet<Option<&str>> = map_agents.iter().map(|a| a.get("id").and_then(Yaml::as_str)).collect();
411
+ if id_set.len() != map_agents.len() {
412
+ e.push("/agents: duplicate agent id".to_string());
413
+ }
414
+ // all_ids:present 的 agent id + leader id(若 truthy)。
415
+ let mut all_ids: HashSet<&str> = map_agents.iter().filter_map(|a| a.get("id").and_then(Yaml::as_str)).collect();
416
+ if let Some(lid) = leader.and_then(|l| l.get("id")).and_then(Yaml::as_str) {
417
+ if !lid.is_empty() {
418
+ all_ids.insert(lid);
419
+ }
420
+ }
421
+
422
+ let leader_provider = leader.and_then(|l| l.get("provider"));
423
+ if !leader_provider.and_then(Yaml::as_str).is_some_and(|p| SUPPORTED_PROVIDERS.contains(&p)) {
424
+ e.push(format!("/leader/provider: unknown provider {}", py_repr(leader_provider)));
425
+ }
426
+
427
+ for (idx, agent) in agents.iter().enumerate() {
428
+ let provider = agent.get("provider");
429
+ if !provider.and_then(Yaml::as_str).is_some_and(|p| SUPPORTED_PROVIDERS.contains(&p)) {
430
+ e.push(format!("/agents/{idx}/provider: unknown provider {}", py_repr(provider)));
431
+ }
432
+ if let Some(auth) = agent.get("auth_mode") {
433
+ if !matches!(auth, Yaml::Null) && !auth.as_str().is_some_and(|a| AUTH_MODES.contains(&a)) {
434
+ e.push(format!("/agents/{idx}/auth_mode: unknown auth_mode {}", py_repr(Some(auth))));
435
+ }
436
+ }
437
+ if let Some(f) = agent.get("system_prompt").and_then(|sp| sp.get("file")).filter(|p| p.is_truthy()).and_then(Yaml::as_str) {
438
+ let candidate = Path::new(f);
439
+ let full = if candidate.is_absolute() { candidate.to_path_buf() } else { base_dir.join(candidate) };
440
+ if !full.exists() {
441
+ e.push(format!("/agents/{idx}/system_prompt/file: file not found: {}", full.display()));
442
+ }
443
+ }
444
+ let tools: Vec<&str> = agent.get("tools").and_then(Yaml::as_list).unwrap_or(&[]).iter().filter_map(Yaml::as_str).collect();
445
+ for tool in permissions::expand_tool_strings(tools) {
446
+ if !permissions::is_canonical_tool(&tool) {
447
+ e.push(format!("/agents/{idx}/tools: unknown tool {}", py_repr_str(&tool)));
448
+ }
449
+ }
450
+ }
451
+
452
+ let leader_tools: Vec<&str> = leader.and_then(|l| l.get("tools")).and_then(Yaml::as_list).unwrap_or(&[]).iter().filter_map(Yaml::as_str).collect();
453
+ for tool in permissions::expand_tool_strings(leader_tools) {
454
+ if !permissions::is_canonical_tool(&tool) {
455
+ e.push(format!("/leader/tools: unknown tool {}", py_repr_str(&tool)));
456
+ }
457
+ }
458
+
459
+ let routing = spec.get("routing");
460
+ if let Some(da) = routing.and_then(|r| r.get("default_assignee")).and_then(Yaml::as_str) {
461
+ if !da.is_empty() && !all_ids.contains(da) {
462
+ e.push(format!("/routing/default_assignee: unknown agent {}", py_repr_str(da)));
463
+ }
464
+ }
465
+ let rules = routing.and_then(|r| r.get("rules")).and_then(Yaml::as_list).unwrap_or(&[]);
466
+ for (idx, rule) in rules.iter().enumerate() {
467
+ let target = rule.get("assign_to");
468
+ if !target.and_then(Yaml::as_str).is_some_and(|t| all_ids.contains(t)) {
469
+ e.push(format!("/routing/rules/{idx}/assign_to: unknown agent {}", py_repr(target)));
470
+ }
471
+ }
472
+
473
+ let tasks: &[Yaml] = spec.get("tasks").and_then(Yaml::as_list).unwrap_or(&[]);
474
+ let task_ids: HashSet<&str> = tasks.iter().filter(|t| t.is_map()).filter_map(|t| t.get("id").and_then(Yaml::as_str)).collect();
475
+ for (idx, task) in tasks.iter().enumerate() {
476
+ if let Some(a) = task.get("assignee").and_then(Yaml::as_str) {
477
+ if !a.is_empty() && !all_ids.contains(a) {
478
+ e.push(format!("/tasks/{idx}/assignee: unknown agent {}", py_repr_str(a)));
479
+ }
480
+ }
481
+ for dep in task.get("deps").and_then(Yaml::as_list).unwrap_or(&[]) {
482
+ if !dep.as_str().is_some_and(|d| task_ids.contains(d)) {
483
+ e.push(format!("/tasks/{idx}/deps: unknown dependency {}", py_repr(Some(dep))));
484
+ }
485
+ }
486
+ }
487
+
488
+ // dependency cycle(只含有非空 id 的 task,复刻 Python `if t.get("id")`)。
489
+ let nodes: Vec<TaskNode> = tasks
490
+ .iter()
491
+ .filter_map(|t| {
492
+ let id = t.get("id").and_then(Yaml::as_str).filter(|s| !s.is_empty())?;
493
+ let deps: Vec<TaskId> = t.get("deps").and_then(Yaml::as_list).unwrap_or(&[]).iter().filter_map(|d| d.as_str().map(TaskId::from)).collect();
494
+ Some(TaskNode::new(TaskId::from(id), deps, TaskStatus::Pending))
495
+ })
496
+ .collect();
497
+ let cycle = find_dependency_cycle(&nodes);
498
+ if !cycle.is_empty() {
499
+ let chain = cycle.iter().map(TaskId::as_str).collect::<Vec<_>>().join(" -> ");
500
+ e.push(format!("/tasks: dependency cycle detected: {chain}"));
501
+ }
502
+ e
503
+ }
504
+
505
+ /// Python `repr()` of a string(选引号 + 转义),用于 `unknown X 'name'`。
506
+ fn py_repr_str(s: &str) -> String {
507
+ let quote = if s.contains('\'') && !s.contains('"') { '"' } else { '\'' };
508
+ let mut out = String::new();
509
+ out.push(quote);
510
+ for c in s.chars() {
511
+ match c {
512
+ '\\' => out.push_str("\\\\"),
513
+ c if c == quote => {
514
+ out.push('\\');
515
+ out.push(c);
516
+ }
517
+ '\n' => out.push_str("\\n"),
518
+ '\r' => out.push_str("\\r"),
519
+ '\t' => out.push_str("\\t"),
520
+ c => out.push(c),
521
+ }
522
+ }
523
+ out.push(quote);
524
+ out
525
+ }
526
+
527
+ /// Python `repr()` of an optional yaml value(None/absent→"None",Str→repr,标量→字面)。
528
+ fn py_repr(v: Option<&Yaml>) -> String {
529
+ match v {
530
+ None | Some(Yaml::Null) => "None".to_string(),
531
+ Some(Yaml::Str(s)) => py_repr_str(s),
532
+ Some(Yaml::Bool(true)) => "True".to_string(),
533
+ Some(Yaml::Bool(false)) => "False".to_string(),
534
+ Some(Yaml::Int(i)) => i.to_string(),
535
+ Some(Yaml::Float(f)) => f.to_string(),
536
+ // list/map 作 provider/dep 等极少见的退化输入;非 Python-exact,仅保证不 panic。
537
+ Some(other) => format!("{other:?}"),
538
+ }
539
+ }
540
+
541
+ #[cfg(test)]
542
+ mod tests {
543
+ #![allow(clippy::unwrap_used)]
544
+ use super::*;
545
+ use serde_json::json;
546
+
547
+ // golden 错误列表由 Python 真相源 `spec._result_schema_errors` 算出(team-agent-public@439bef8)。
548
+ // §4.2 行为 diff 双跑:Rust 必须产出逐字节一致的有序消息。
549
+ #[test]
550
+ fn valid_envelope_has_no_errors() {
551
+ let v = json!({
552
+ "schema_version":"result_envelope_v1","task_id":"t1","agent_id":"a1",
553
+ "status":"success","summary":"done",
554
+ "changes":[],"tests":[],"risks":[],"artifacts":[],"next_actions":[]
555
+ });
556
+ assert!(result_schema_errors(&v).is_empty());
557
+ assert!(validate_result_envelope(&v).is_ok());
558
+ }
559
+
560
+ #[test]
561
+ fn missing_unknown_status_schema_order_matches_python() {
562
+ let v = json!({
563
+ "schema_version":"v0","task_id":"","status":"weird","summary":"s","schema":"x",
564
+ "changes":[],"tests":[],"risks":[],"artifacts":[],"next_actions":[]
565
+ });
566
+ assert_eq!(
567
+ result_schema_errors(&v),
568
+ vec![
569
+ "/agent_id: missing required field",
570
+ "/schema: unknown field",
571
+ "/schema_version: must be result_envelope_v1",
572
+ "/task_id: must not be empty",
573
+ "/status: invalid result status",
574
+ "/schema: use schema_version, not schema",
575
+ ]
576
+ );
577
+ }
578
+
579
+ #[test]
580
+ fn non_object_envelope() {
581
+ assert_eq!(
582
+ result_schema_errors(&json!(["x"])),
583
+ vec!["/: must be an object"]
584
+ );
585
+ }
586
+
587
+ #[test]
588
+ fn bad_collection_items_match_python() {
589
+ let v = json!({
590
+ "schema_version":"result_envelope_v1","task_id":"t","agent_id":"a",
591
+ "status":"success","summary":"s",
592
+ "changes":[{"path":"p","kind":"bogus","description":"d"}],
593
+ "tests":[{"command":"c","status":"weird"}],
594
+ "risks":[{"severity":"huge","description":"d"}],
595
+ "artifacts":[],
596
+ "next_actions":[{"description":123}]
597
+ });
598
+ assert_eq!(
599
+ result_schema_errors(&v),
600
+ vec![
601
+ "/changes/0/kind: invalid change kind",
602
+ "/tests/0/status: invalid test status",
603
+ "/risks/0/severity: invalid risk severity",
604
+ "/next_actions/0/description: must be a string",
605
+ ]
606
+ );
607
+ }
608
+
609
+ #[test]
610
+ fn validate_returns_joined_message() {
611
+ let v = json!(["x"]);
612
+ let err = validate_result_envelope(&v).unwrap_err();
613
+ assert_eq!(
614
+ err.to_string(),
615
+ "validation error: result_envelope_v1 validation failed:\n- /: must be an object"
616
+ );
617
+ }
618
+
619
+ // --- validate_spec(对 Python `spec.validate_spec` 取 golden) ---
620
+
621
+ const TD: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/src/model/testdata");
622
+
623
+ fn all_spec_errors(spec: &Yaml) -> Vec<String> {
624
+ let mut v = basic_schema_errors(spec);
625
+ v.extend(semantic_errors(spec, Path::new(TD)));
626
+ v
627
+ }
628
+
629
+ #[test]
630
+ fn valid_team_spec_passes() {
631
+ let spec = yaml::loads(include_str!("testdata/team.spec.yaml")).unwrap();
632
+ let r = validate_spec(&spec, Path::new(TD));
633
+ assert!(r.is_ok(), "expected valid, got: {r:?}");
634
+ }
635
+
636
+ #[test]
637
+ fn invalid_spec_a_matches_python_golden() {
638
+ let spec = yaml::loads(include_str!("testdata/spec_invalid_a.yaml")).unwrap();
639
+ assert_eq!(
640
+ all_spec_errors(&spec),
641
+ vec![
642
+ "/version: must equal 1",
643
+ "/team/mode: invalid mode",
644
+ "/leader/provider: unknown provider 'badprov'",
645
+ "/agents/0/tools: unknown tool 'banana'",
646
+ ]
647
+ );
648
+ }
649
+
650
+ #[test]
651
+ fn empty_spec_matches_python_golden() {
652
+ let spec = yaml::loads("{}").unwrap();
653
+ assert_eq!(
654
+ all_spec_errors(&spec),
655
+ vec![
656
+ "/agents: missing required field",
657
+ "/communication: missing required field",
658
+ "/context: missing required field",
659
+ "/leader: missing required field",
660
+ "/routing: missing required field",
661
+ "/runtime: missing required field",
662
+ "/tasks: missing required field",
663
+ "/team: missing required field",
664
+ "/version: missing required field",
665
+ "/version: must equal 1",
666
+ "/team: must be an object",
667
+ "/team/mode: invalid mode",
668
+ "/leader: must be an object",
669
+ "/leader/context_policy: must be an object",
670
+ "/agents: must be a non-empty list",
671
+ "/routing: must be an object",
672
+ "/communication: must be an object",
673
+ "/runtime: must be an object",
674
+ "/context: must be an object",
675
+ "/tasks: must be a list",
676
+ "/leader/provider: unknown provider None",
677
+ ]
678
+ );
679
+ }
680
+ }