@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,701 @@
1
+ #![allow(clippy::unwrap_used)]
2
+ use super::*;
3
+ use crate::model::spec;
4
+ use std::fs;
5
+ use std::path::PathBuf;
6
+ use std::sync::atomic::{AtomicU32, Ordering};
7
+
8
+ static COUNTER: AtomicU32 = AtomicU32::new(0);
9
+
10
+ fn unique_base() -> PathBuf {
11
+ let n = COUNTER.fetch_add(1, Ordering::Relaxed);
12
+ std::env::temp_dir().join(format!("ta_rs_compile_{}_{}", std::process::id(), n))
13
+ }
14
+
15
+ /// Build `<tmp>/.team/current/{TEAM.md, agents/*, profiles/*}` and return the
16
+ /// team dir. `team_workspace(team_dir)` → `<tmp>` (parent is `.team`).
17
+ fn build_team(team_md: &str, roles: &[(&str, &str)], profiles: &[(&str, &str)]) -> PathBuf {
18
+ let team = unique_base().join(".team").join("current");
19
+ fs::create_dir_all(team.join("agents")).unwrap();
20
+ fs::create_dir_all(team.join("profiles")).unwrap();
21
+ fs::write(team.join("TEAM.md"), team_md).unwrap();
22
+ for (name, text) in roles {
23
+ fs::write(team.join("agents").join(name), text).unwrap();
24
+ }
25
+ for (name, text) in profiles {
26
+ fs::write(team.join("profiles").join(name), text).unwrap();
27
+ }
28
+ team
29
+ }
30
+
31
+ fn write_tmp(name: &str, text: &str) -> PathBuf {
32
+ let base = unique_base();
33
+ fs::create_dir_all(&base).unwrap();
34
+ let p = base.join(name);
35
+ fs::write(&p, text).unwrap();
36
+ p
37
+ }
38
+
39
+ /// `json.dumps(v, sort_keys=False, separators=(",",":"))` for a `yaml::Value`:
40
+ /// preserves Map insertion order; string escaping matches Python json (ASCII
41
+ /// content here). Locks both VALUE and KEY INSERTION ORDER of the spec dict.
42
+ fn compact_json(v: &Value) -> String {
43
+ match v {
44
+ Value::Null => "null".to_string(),
45
+ Value::Bool(b) => b.to_string(),
46
+ Value::Int(i) => i.to_string(),
47
+ Value::Float(f) => format!("{f}"),
48
+ Value::Str(s) => serde_json::to_string(s).unwrap(),
49
+ Value::List(items) => {
50
+ let inner: Vec<String> = items.iter().map(compact_json).collect();
51
+ format!("[{}]", inner.join(","))
52
+ }
53
+ Value::Map(pairs) => {
54
+ let inner: Vec<String> = pairs
55
+ .iter()
56
+ .map(|(k, val)| format!("{}:{}", serde_json::to_string(k).unwrap(), compact_json(val)))
57
+ .collect();
58
+ format!("{{{}}}", inner.join(","))
59
+ }
60
+ }
61
+ }
62
+
63
+ /// Render spec to compact JSON with the env-dependent workspace path templated
64
+ /// to `__WS__` (appears as both `team.workspace` and each `working_directory`).
65
+ fn templated_compact_json(spec: &Value) -> String {
66
+ let ws = spec
67
+ .get("team")
68
+ .and_then(|t| t.get("workspace"))
69
+ .and_then(Value::as_str)
70
+ .expect("spec.team.workspace must be a string");
71
+ compact_json(spec).replace(ws, "__WS__")
72
+ }
73
+
74
+ fn workspace_of(spec: &Value) -> String {
75
+ spec.get("team")
76
+ .and_then(|t| t.get("workspace"))
77
+ .and_then(Value::as_str)
78
+ .unwrap()
79
+ .to_string()
80
+ }
81
+
82
+ // ───────────────────── fixtures (byte-identical to the Python probe) ─────────────────────
83
+
84
+ const TEAM_BASE: &str = "\
85
+ ---
86
+ name: doc-team
87
+ objective: Compile role docs.
88
+ provider: codex
89
+ model: gpt-5.5
90
+ ---
91
+
92
+ Document-driven team.
93
+ ";
94
+
95
+ const ROLE_NOPROFILE: &str = "\
96
+ ---
97
+ name: implementer
98
+ role: Implementation Engineer
99
+ provider: codex
100
+ model: gpt-5.5
101
+ auth_mode: subscription
102
+ tools:
103
+ - fs_read
104
+ - fs_write
105
+ - execute_bash
106
+ - mcp_team
107
+ ---
108
+
109
+ Implement bounded tasks and report result_envelope_v1.
110
+ ";
111
+
112
+ // ───────────────────────────── read_front_matter ─────────────────────────────
113
+
114
+ #[test]
115
+ fn front_matter_no_marker_returns_empty_meta_and_full_text() {
116
+ // No leading "---\n" → ({}, text) verbatim (compiler.py:175-176).
117
+ let p = write_tmp("no_marker.md", "hello\nworld\n");
118
+ let (meta, body) = read_front_matter(&p).unwrap();
119
+ assert_eq!(meta, Value::Map(vec![]));
120
+ assert_eq!(body, "hello\nworld\n");
121
+ }
122
+
123
+ #[test]
124
+ fn front_matter_basic_splits_meta_and_lstrips_body() {
125
+ let p = write_tmp("basic.md", "---\nname: x\nrole: R\n---\n\nbody line\n");
126
+ let (meta, body) = read_front_matter(&p).unwrap();
127
+ assert_eq!(
128
+ meta,
129
+ Value::Map(vec![
130
+ ("name".to_string(), Value::Str("x".to_string())),
131
+ ("role".to_string(), Value::Str("R".to_string())),
132
+ ])
133
+ );
134
+ assert_eq!(body, "body line\n");
135
+ }
136
+
137
+ #[test]
138
+ fn front_matter_empty_block_is_empty_map() {
139
+ // "---\n\n---\nbody\n": closing marker at the blank line → raw "" → {} ; body "body\n".
140
+ let p = write_tmp("empty.md", "---\n\n---\nbody\n");
141
+ let (meta, body) = read_front_matter(&p).unwrap();
142
+ assert_eq!(meta, Value::Map(vec![]));
143
+ assert_eq!(body, "body\n");
144
+ }
145
+
146
+ #[test]
147
+ fn front_matter_body_lstrip_strips_only_newlines() {
148
+ // body.lstrip("\n") removes leading NEWLINES but keeps the 2-space indent.
149
+ let p = write_tmp("lstrip.md", "---\nname: x\n---\n\n\n indented body\n");
150
+ let (meta, body) = read_front_matter(&p).unwrap();
151
+ assert_eq!(meta, Value::Map(vec![("name".to_string(), Value::Str("x".to_string()))]));
152
+ assert_eq!(body, " indented body\n");
153
+ }
154
+
155
+ #[test]
156
+ fn front_matter_unterminated_errors() {
157
+ let p = write_tmp("unterminated.md", "---\nname: x\n");
158
+ let err = read_front_matter(&p).unwrap_err();
159
+ assert!(
160
+ err.to_string().contains("unterminated front matter"),
161
+ "got: {err}"
162
+ );
163
+ }
164
+
165
+ #[test]
166
+ fn front_matter_non_object_errors() {
167
+ // A YAML list in the block → "front matter must be a YAML object" (compiler.py:183-184).
168
+ let p = write_tmp("list.md", "---\n- a\n- b\n---\nbody\n");
169
+ let err = read_front_matter(&p).unwrap_err();
170
+ assert!(
171
+ err.to_string().contains("front matter must be a YAML object"),
172
+ "got: {err}"
173
+ );
174
+ }
175
+
176
+ // ───────────────────────────── compile_team: full dict parity ─────────────────────────────
177
+
178
+ // Golden = Python `json.dumps(compile_team(team)["spec"], sort_keys=False,
179
+ // separators=(",",":"))` with the workspace path templated to __WS__.
180
+ // (team-agent-public v0.2.11, /tmp/probe_compiler.py.)
181
+
182
+ const BASE_NOPROFILE_JSON: &str = r#"{"version":1,"team":{"name":"doc-team","mode":"supervisor_worker","objective":"Compile role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":"gpt-5.5","tools":["fs_read","fs_list","mcp_team"],"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"implementer","role":"Implementation Engineer","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Implement bounded tasks and report result_envelope_v1.","file":null},"tools":["fs_read","fs_write","execute_bash","mcp_team"],"permission_mode":"restricted","preferred_for":["implementer","Implementation Engineer"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"implementer","rules":[{"id":"route-implementer","match":{"assignee":["implementer"]},"assign_to":"implementer","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","display_backend":"adaptive","session_name":"team-doc-team","auto_launch":true,"require_user_approval_before_launch":true,"max_active_agents":1,"startup_order":["implementer"],"dangerous_auto_approve":false,"fast":false,"tick_interval_sec":2,"push_min_interval_sec":60,"stuck_timeout_sec":300},"context":{"state_file":"team_state.md","artifact_dir":".team/artifacts","log_dir":".team/logs","summarization":{"worker_full_logs":"retain_outside_leader_context","state_update":"after_each_result"}},"tasks":[{"id":"task_initial","title":"Initial document-driven team task","type":"implementation","assignee":"implementer","deps":[],"acceptance":["Worker reports valid result_envelope_v1"],"status":"pending","requires_tools":["mcp_team"],"files":[],"risk":"low"}]}"#;
183
+
184
+ #[test]
185
+ fn compile_base_noprofile_matches_python_dict_order_and_values() {
186
+ let team = build_team(TEAM_BASE, &[("implementer.md", ROLE_NOPROFILE)], &[]);
187
+ let spec = compile_team(&team).unwrap();
188
+ assert_eq!(templated_compact_json(&spec), BASE_NOPROFILE_JSON);
189
+ }
190
+
191
+ #[test]
192
+ fn compile_base_returned_spec_passes_validate_spec() {
193
+ // §6 contract: the compiled spec MUST pass model::spec::validate_spec.
194
+ let team = build_team(TEAM_BASE, &[("implementer.md", ROLE_NOPROFILE)], &[]);
195
+ let spec = compile_team(&team).unwrap();
196
+ let ws = workspace_of(&spec);
197
+ assert!(spec::validate_spec(&spec, Path::new(&ws)).is_ok());
198
+ }
199
+
200
+ #[test]
201
+ fn compile_subscription_without_profile_is_thin_manifest() {
202
+ // No profile field → agent has auth_mode=subscription and NO profile / credential_ref keys.
203
+ let team = build_team(TEAM_BASE, &[("implementer.md", ROLE_NOPROFILE)], &[]);
204
+ let spec = compile_team(&team).unwrap();
205
+ let agent = &spec.get("agents").and_then(Value::as_list).unwrap()[0];
206
+ assert_eq!(agent.get("auth_mode").and_then(Value::as_str), Some("subscription"));
207
+ assert!(agent.get("profile").is_none(), "profile key must be absent");
208
+ assert!(agent.get("credential_ref").is_none(), "credential_ref key must be absent");
209
+ }
210
+
211
+ // Runtime front-matter defaults: every knob from TEAM.md flows through without
212
+ // per-role repetition; thin role inherits default_model; leader.model is null
213
+ // (TEAM.md has no `model:` key, only `default_model:`).
214
+ const RUNTIME_DEFAULTS_TEAM: &str = "\
215
+ ---
216
+ name: doc-team
217
+ objective: Compile role docs.
218
+ provider: codex
219
+ default_model: gpt-5.4
220
+ default_auth_mode: subscription
221
+ dangerous_auto_approve: true
222
+ fast: true
223
+ display_backend: ghostty_window
224
+ tick_interval_sec: 1
225
+ push_min_interval_sec: 3
226
+ stuck_timeout_sec: 5
227
+ worker_to_worker: true
228
+ ---
229
+
230
+ Document-driven team.
231
+ ";
232
+
233
+ const RUNTIME_DEFAULTS_ROLE: &str = "\
234
+ ---
235
+ name: implementer
236
+ role: Implementation Engineer
237
+ provider: codex
238
+ tools:
239
+ - fs_read
240
+ - mcp_team
241
+ ---
242
+
243
+ Implement bounded tasks.
244
+ ";
245
+
246
+ const RUNTIME_DEFAULTS_JSON: &str = r#"{"version":1,"team":{"name":"doc-team","mode":"supervisor_worker","objective":"Compile role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":null,"tools":["fs_read","fs_list","mcp_team"],"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"implementer","role":"Implementation Engineer","provider":"codex","model":"gpt-5.4","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Implement bounded tasks.","file":null},"tools":["fs_read","mcp_team"],"permission_mode":"restricted","preferred_for":["implementer","Implementation Engineer"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"implementer","rules":[{"id":"route-implementer","match":{"assignee":["implementer"]},"assign_to":"implementer","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","display_backend":"ghostty_window","session_name":"team-doc-team","auto_launch":true,"require_user_approval_before_launch":true,"max_active_agents":1,"startup_order":["implementer"],"dangerous_auto_approve":true,"fast":true,"tick_interval_sec":1,"push_min_interval_sec":3,"stuck_timeout_sec":5},"context":{"state_file":"team_state.md","artifact_dir":".team/artifacts","log_dir":".team/logs","summarization":{"worker_full_logs":"retain_outside_leader_context","state_update":"after_each_result"}},"tasks":[{"id":"task_initial","title":"Initial document-driven team task","type":"implementation","assignee":"implementer","deps":[],"acceptance":["Worker reports valid result_envelope_v1"],"status":"pending","requires_tools":["mcp_team"],"files":[],"risk":"low"}]}"#;
247
+
248
+ #[test]
249
+ fn compile_runtime_front_matter_defaults_match_python() {
250
+ let team = build_team(RUNTIME_DEFAULTS_TEAM, &[("implementer.md", RUNTIME_DEFAULTS_ROLE)], &[]);
251
+ let spec = compile_team(&team).unwrap();
252
+ assert_eq!(templated_compact_json(&spec), RUNTIME_DEFAULTS_JSON);
253
+ }
254
+
255
+ // provider_models alias ladder: role provider `claude_code` with NO `claude_code`
256
+ // key in provider_models falls back to the `claude` key (compiler.py:317-318).
257
+ const ALIAS_TEAM: &str = "\
258
+ ---
259
+ name: debate-team
260
+ objective: Compile thin role docs.
261
+ provider_models:
262
+ claude: claude-sonnet-4-6
263
+ default_auth_mode: subscription
264
+ display_backend: none
265
+ ---
266
+
267
+ Team config.
268
+ ";
269
+
270
+ const ALIAS_ROLE: &str = "\
271
+ ---
272
+ name: editor
273
+ role: Editor and Defender
274
+ provider: claude_code
275
+ tools:
276
+ - mcp_team
277
+ ---
278
+
279
+ Edit and defend the argument.
280
+ ";
281
+
282
+ const ALIAS_JSON: &str = r#"{"version":1,"team":{"name":"debate-team","mode":"supervisor_worker","objective":"Compile thin role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":null,"tools":["fs_read","fs_list","mcp_team"],"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"editor","role":"Editor and Defender","provider":"claude_code","model":"claude-sonnet-4-6","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Edit and defend the argument.","file":null},"tools":["mcp_team"],"permission_mode":"restricted","preferred_for":["editor","Editor and Defender"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"editor","rules":[{"id":"route-editor","match":{"assignee":["editor"]},"assign_to":"editor","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","display_backend":"none","session_name":"team-debate-team","auto_launch":true,"require_user_approval_before_launch":true,"max_active_agents":1,"startup_order":["editor"],"dangerous_auto_approve":false,"fast":false,"tick_interval_sec":2,"push_min_interval_sec":60,"stuck_timeout_sec":300},"context":{"state_file":"team_state.md","artifact_dir":".team/artifacts","log_dir":".team/logs","summarization":{"worker_full_logs":"retain_outside_leader_context","state_update":"after_each_result"}},"tasks":[{"id":"task_initial","title":"Initial document-driven team task","type":"implementation","assignee":"editor","deps":[],"acceptance":["Worker reports valid result_envelope_v1"],"status":"pending","requires_tools":["mcp_team"],"files":[],"risk":"low"}]}"#;
283
+
284
+ #[test]
285
+ fn compile_provider_models_claude_code_falls_back_to_claude_alias() {
286
+ let team = build_team(ALIAS_TEAM, &[("implementer.md", ALIAS_ROLE)], &[]);
287
+ let spec = compile_team(&team).unwrap();
288
+ assert_eq!(templated_compact_json(&spec), ALIAS_JSON);
289
+ }
290
+
291
+ // Builtin provider default: no model anywhere → DEFAULT_PROVIDER_MODELS[codex] = gpt-5.5.
292
+ const BUILTIN_TEAM: &str = "\
293
+ ---
294
+ name: default-model-team
295
+ objective: Compile role docs without model fields.
296
+ display_backend: none
297
+ ---
298
+
299
+ Team config.
300
+ ";
301
+
302
+ const BUILTIN_ROLE: &str = "\
303
+ ---
304
+ name: implementer
305
+ role: Implementation Engineer
306
+ provider: codex
307
+ auth_mode: subscription
308
+ tools:
309
+ - mcp_team
310
+ ---
311
+
312
+ Implement bounded tasks.
313
+ ";
314
+
315
+ const BUILTIN_JSON: &str = r#"{"version":1,"team":{"name":"default-model-team","mode":"supervisor_worker","objective":"Compile role docs without model fields.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":null,"tools":["fs_read","fs_list","mcp_team"],"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"implementer","role":"Implementation Engineer","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Implement bounded tasks.","file":null},"tools":["mcp_team"],"permission_mode":"restricted","preferred_for":["implementer","Implementation Engineer"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"implementer","rules":[{"id":"route-implementer","match":{"assignee":["implementer"]},"assign_to":"implementer","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","display_backend":"none","session_name":"team-default-model-team","auto_launch":true,"require_user_approval_before_launch":true,"max_active_agents":1,"startup_order":["implementer"],"dangerous_auto_approve":false,"fast":false,"tick_interval_sec":2,"push_min_interval_sec":60,"stuck_timeout_sec":300},"context":{"state_file":"team_state.md","artifact_dir":".team/artifacts","log_dir":".team/logs","summarization":{"worker_full_logs":"retain_outside_leader_context","state_update":"after_each_result"}},"tasks":[{"id":"task_initial","title":"Initial document-driven team task","type":"implementation","assignee":"implementer","deps":[],"acceptance":["Worker reports valid result_envelope_v1"],"status":"pending","requires_tools":["mcp_team"],"files":[],"risk":"low"}]}"#;
316
+
317
+ #[test]
318
+ fn compile_subscription_without_model_uses_builtin_provider_default() {
319
+ let team = build_team(BUILTIN_TEAM, &[("implementer.md", BUILTIN_ROLE)], &[]);
320
+ let spec = compile_team(&team).unwrap();
321
+ assert_eq!(templated_compact_json(&spec), BUILTIN_JSON);
322
+ }
323
+
324
+ // Two role docs sorted by FILENAME (01-alpha before 02-bravo though names alpha/bravo):
325
+ // startup_order, routing rules (one per agent), default_assignee = first agent,
326
+ // max_active_agents = min(len, 2).
327
+ const TWO_ROLE_A: &str = "\
328
+ ---
329
+ name: alpha
330
+ role: Alpha Worker
331
+ provider: codex
332
+ model: gpt-5.5
333
+ auth_mode: subscription
334
+ tools:
335
+ - mcp_team
336
+ ---
337
+
338
+ Alpha body.
339
+ ";
340
+
341
+ const TWO_ROLE_B: &str = "\
342
+ ---
343
+ name: bravo
344
+ role: Bravo Worker
345
+ provider: codex
346
+ model: gpt-5.5
347
+ auth_mode: subscription
348
+ tools:
349
+ - mcp_team
350
+ ---
351
+
352
+ Bravo body.
353
+ ";
354
+
355
+ const TWO_AGENTS_JSON: &str = r#"{"version":1,"team":{"name":"doc-team","mode":"supervisor_worker","objective":"Compile role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":"gpt-5.5","tools":["fs_read","fs_list","mcp_team"],"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"alpha","role":"Alpha Worker","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Alpha body.","file":null},"tools":["mcp_team"],"permission_mode":"restricted","preferred_for":["alpha","Alpha Worker"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}},{"id":"bravo","role":"Bravo Worker","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Bravo body.","file":null},"tools":["mcp_team"],"permission_mode":"restricted","preferred_for":["bravo","Bravo Worker"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"alpha","rules":[{"id":"route-alpha","match":{"assignee":["alpha"]},"assign_to":"alpha","priority":10},{"id":"route-bravo","match":{"assignee":["bravo"]},"assign_to":"bravo","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","display_backend":"adaptive","session_name":"team-doc-team","auto_launch":true,"require_user_approval_before_launch":true,"max_active_agents":2,"startup_order":["alpha","bravo"],"dangerous_auto_approve":false,"fast":false,"tick_interval_sec":2,"push_min_interval_sec":60,"stuck_timeout_sec":300},"context":{"state_file":"team_state.md","artifact_dir":".team/artifacts","log_dir":".team/logs","summarization":{"worker_full_logs":"retain_outside_leader_context","state_update":"after_each_result"}},"tasks":[{"id":"task_initial","title":"Initial document-driven team task","type":"implementation","assignee":"alpha","deps":[],"acceptance":["Worker reports valid result_envelope_v1"],"status":"pending","requires_tools":["mcp_team"],"files":[],"risk":"low"}]}"#;
356
+
357
+ #[test]
358
+ fn compile_two_agents_sorted_by_filename_with_routing_and_startup_order() {
359
+ // filenames intentionally out of name order to prove sorted(glob) is by filename.
360
+ let team = build_team(TEAM_BASE, &[("02-bravo.md", TWO_ROLE_B), ("01-alpha.md", TWO_ROLE_A)], &[]);
361
+ let spec = compile_team(&team).unwrap();
362
+ assert_eq!(templated_compact_json(&spec), TWO_AGENTS_JSON);
363
+ }
364
+
365
+ // ───────────────────────────── compile_team: error paths ─────────────────────────────
366
+
367
+ const ROLE_MISSING_PROVIDER: &str = "\
368
+ ---
369
+ name: implementer
370
+ role: Implementation Engineer
371
+ model: gpt-5.5
372
+ auth_mode: subscription
373
+ tools:
374
+ - mcp_team
375
+ ---
376
+
377
+ Implement bounded tasks.
378
+ ";
379
+
380
+ #[test]
381
+ fn compile_missing_required_provider_field_errors() {
382
+ let team = build_team(TEAM_BASE, &[("implementer.md", ROLE_MISSING_PROVIDER)], &[]);
383
+ let err = compile_team(&team).unwrap_err();
384
+ assert!(
385
+ err.to_string().contains("missing front matter field provider"),
386
+ "got: {err}"
387
+ );
388
+ }
389
+
390
+ const ROLE_COMPATIBLE_NO_PROFILE: &str = "\
391
+ ---
392
+ name: implementer
393
+ role: Implementation Engineer
394
+ provider: codex
395
+ model: gpt-5.5
396
+ auth_mode: compatible_api
397
+ tools:
398
+ - mcp_team
399
+ ---
400
+
401
+ Implement bounded tasks.
402
+ ";
403
+
404
+ #[test]
405
+ fn compile_compatible_api_without_profile_errors() {
406
+ let team = build_team(TEAM_BASE, &[("implementer.md", ROLE_COMPATIBLE_NO_PROFILE)], &[]);
407
+ let err = compile_team(&team).unwrap_err();
408
+ assert!(err.to_string().contains("profile is required"), "got: {err}");
409
+ }
410
+
411
+ // ════════════════════════ FIX-LOOP (wave-1) RED tests ════════════════════════
412
+ // The first-round fixtures under-specified the contract. These lock the CORRECT
413
+ // Python v0.2.11 behavior; each FAILS against the current (too-narrow) impl.
414
+ // Golden re-probed via /tmp/probe_fix.py against team-agent-public v0.2.11.
415
+
416
+ const TM_CODEX: &str = "---\nname: T\nprovider: codex\n---\nx\n";
417
+
418
+ fn role_nomodel(provider: &str) -> String {
419
+ format!("---\nname: w\nrole: R\nprovider: {provider}\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nbody\n")
420
+ }
421
+
422
+ fn agent0(spec: &Value) -> &Value {
423
+ match spec.get("agents") {
424
+ Some(Value::List(items)) if !items.is_empty() => &items[0],
425
+ _ => panic!("spec.agents missing/empty"),
426
+ }
427
+ }
428
+
429
+ fn get_path<'a>(v: &'a Value, keys: &[&str]) -> Option<&'a Value> {
430
+ let mut cur = v;
431
+ for k in keys {
432
+ cur = cur.get(k)?;
433
+ }
434
+ Some(cur)
435
+ }
436
+
437
+ fn str_path(spec: &Value, keys: &[&str]) -> String {
438
+ get_path(spec, keys)
439
+ .and_then(Value::as_str)
440
+ .unwrap_or_else(|| panic!("missing string at {keys:?}"))
441
+ .to_string()
442
+ }
443
+
444
+ enum AgentsDir<'a> {
445
+ Missing,
446
+ File,
447
+ WithRole(&'a str),
448
+ }
449
+
450
+ /// Build `<base>/<parent>/<leaf>/` with optional TEAM.md and a controllable
451
+ /// `agents` entry — lets tests pin `team_dir.parent.name` (A3) and the
452
+ /// missing-TEAM.md / missing-agents / agents-is-a-file error paths (A11).
453
+ fn build_layout(parent: &str, leaf: &str, team_md: Option<&str>, agents: AgentsDir<'_>) -> PathBuf {
454
+ let team = unique_base().join(parent).join(leaf);
455
+ fs::create_dir_all(&team).unwrap();
456
+ if let Some(md) = team_md {
457
+ fs::write(team.join("TEAM.md"), md).unwrap();
458
+ }
459
+ match agents {
460
+ AgentsDir::Missing => {}
461
+ AgentsDir::File => fs::write(team.join("agents"), "not a dir").unwrap(),
462
+ AgentsDir::WithRole(role) => {
463
+ fs::create_dir_all(team.join("agents")).unwrap();
464
+ fs::write(team.join("agents").join("w.md"), role).unwrap();
465
+ }
466
+ }
467
+ team
468
+ }
469
+
470
+ // ── A1 model resolution ──
471
+
472
+ #[test]
473
+ fn fix_a1_provider_models_precede_default_model() {
474
+ // precedence: provider_models[provider] BEFORE default_model (current swaps them).
475
+ let tm = "---\nname: T\nprovider: codex\ndefault_model: team-y\nprovider_models:\n codex: pm-z\n---\nx\n";
476
+ let team = build_team(tm, &[("w.md", &role_nomodel("codex"))], &[]);
477
+ let spec = compile_team(&team).unwrap();
478
+ assert_eq!(agent0(&spec).get("model").and_then(Value::as_str), Some("pm-z"));
479
+ }
480
+
481
+ #[test]
482
+ fn fix_a1_claude_aliases_to_claude_code_provider_models() {
483
+ // TWO-WAY alias: provider `claude` consumes `provider_models[claude_code]`.
484
+ let tm = "---\nname: T\nprovider: codex\nprovider_models:\n claude_code: cc-v\n---\nx\n";
485
+ let team = build_team(tm, &[("w.md", &role_nomodel("claude"))], &[]);
486
+ let spec = compile_team(&team).unwrap();
487
+ assert_eq!(agent0(&spec).get("model").and_then(Value::as_str), Some("cc-v"));
488
+ }
489
+
490
+ #[test]
491
+ fn fix_a1_builtin_claude_default_is_sonnet_4_6() {
492
+ // DEFAULT_PROVIDER_MODELS: claude / claude_code → "claude-sonnet-4-6" (not -4-5).
493
+ for prov in ["claude", "claude_code"] {
494
+ let team = build_team(TM_CODEX, &[("w.md", &role_nomodel(prov))], &[]);
495
+ let spec = compile_team(&team).unwrap();
496
+ assert_eq!(
497
+ agent0(&spec).get("model").and_then(Value::as_str),
498
+ Some("claude-sonnet-4-6"),
499
+ "provider {prov}"
500
+ );
501
+ }
502
+ }
503
+
504
+ #[test]
505
+ fn fix_a1_model_null_when_provider_absent_from_table() {
506
+ // gemini_cli / fake have NO builtin default → model MUST be emitted as null.
507
+ for prov in ["gemini_cli", "fake"] {
508
+ let team = build_team(TM_CODEX, &[("w.md", &role_nomodel(prov))], &[]);
509
+ let spec = compile_team(&team).unwrap();
510
+ assert_eq!(agent0(&spec).get("model"), Some(&Value::Null), "provider {prov} → null");
511
+ }
512
+ }
513
+
514
+ // ── A2 objective ──
515
+
516
+ #[test]
517
+ fn fix_a2_objective_falls_back_to_body() {
518
+ let tm = "---\nname: T\nprovider: codex\n---\nThis is the body.\n";
519
+ let team = build_team(tm, &[("w.md", &role_nomodel("codex"))], &[]);
520
+ let spec = compile_team(&team).unwrap();
521
+ assert_eq!(str_path(&spec, &["team", "objective"]), "This is the body.");
522
+ }
523
+
524
+ #[test]
525
+ fn fix_a2_objective_default_when_no_objective_and_no_body() {
526
+ let tm = "---\nname: T\nprovider: codex\n---\n";
527
+ let team = build_team(tm, &[("w.md", &role_nomodel("codex"))], &[]);
528
+ let spec = compile_team(&team).unwrap();
529
+ assert_eq!(str_path(&spec, &["team", "objective"]), "Team Agent document-driven team.");
530
+ }
531
+
532
+ // ── A3 name ──
533
+
534
+ #[test]
535
+ fn fix_a3_name_falls_back_to_parent_dir_name() {
536
+ // no `name` → team_dir.parent.name (NOT a hardcoded "team").
537
+ let tm = "---\nprovider: codex\n---\nx\n";
538
+ let team = build_layout("my-parent-dir", "leafteam", Some(tm), AgentsDir::WithRole(&role_nomodel("codex")));
539
+ let spec = compile_team(&team).unwrap();
540
+ assert_eq!(str_path(&spec, &["team", "name"]), "my-parent-dir");
541
+ }
542
+
543
+ // ── A4 leader.role ──
544
+
545
+ #[test]
546
+ fn fix_a4_leader_role_from_team_meta() {
547
+ let tm = "---\nname: T\nprovider: codex\nleader_role: Captain\n---\nx\n";
548
+ let team = build_team(tm, &[("w.md", &role_nomodel("codex"))], &[]);
549
+ let spec = compile_team(&team).unwrap();
550
+ assert_eq!(str_path(&spec, &["leader", "role"]), "Captain");
551
+ }
552
+
553
+ // ── A5 session_name + _slug ──
554
+
555
+ #[test]
556
+ fn fix_a5_session_name_slugifies_team_name() {
557
+ // "My Team!" → _slug → "My-Team" → "team-My-Team".
558
+ let tm = "---\nname: My Team!\nprovider: codex\n---\nx\n";
559
+ let team = build_team(tm, &[("w.md", &role_nomodel("codex"))], &[]);
560
+ let spec = compile_team(&team).unwrap();
561
+ assert_eq!(str_path(&spec, &["runtime", "session_name"]), "team-My-Team");
562
+ }
563
+
564
+ #[test]
565
+ fn fix_a5_session_name_override_wins() {
566
+ let tm = "---\nname: My Team!\nprovider: codex\nsession_name: custom-sess\n---\nx\n";
567
+ let team = build_team(tm, &[("w.md", &role_nomodel("codex"))], &[]);
568
+ let spec = compile_team(&team).unwrap();
569
+ assert_eq!(str_path(&spec, &["runtime", "session_name"]), "custom-sess");
570
+ }
571
+
572
+ // ── A6 tools ──
573
+
574
+ #[test]
575
+ fn fix_a6_tools_shell_maps_to_execute_bash() {
576
+ let role = "---\nname: w\nrole: R\nprovider: codex\nauth_mode: subscription\ntools:\n - shell\n - mcp_team\n---\nb\n";
577
+ let team = build_team(TM_CODEX, &[("w.md", role)], &[]);
578
+ let spec = compile_team(&team).expect("shell must normalize to execute_bash and compile");
579
+ assert_eq!(agent0(&spec).get("tools"), Some(&list_str(vec!["execute_bash", "mcp_team"])));
580
+ }
581
+
582
+ #[test]
583
+ fn fix_a6_missing_tools_errors() {
584
+ let role = "---\nname: w\nrole: R\nprovider: codex\nauth_mode: subscription\n---\nb\n";
585
+ let team = build_team(TM_CODEX, &[("w.md", role)], &[]);
586
+ let err = compile_team(&team).unwrap_err();
587
+ assert!(err.to_string().contains("missing front matter field tools"), "got: {err}");
588
+ }
589
+
590
+ #[test]
591
+ fn fix_a6_tools_not_a_list_errors() {
592
+ let role = "---\nname: w\nrole: R\nprovider: codex\nauth_mode: subscription\ntools: justastring\n---\nb\n";
593
+ let team = build_team(TM_CODEX, &[("w.md", role)], &[]);
594
+ let err = compile_team(&team).unwrap_err();
595
+ assert!(err.to_string().contains("tools must be a list"), "got: {err}");
596
+ }
597
+
598
+ // ── A7 system_prompt inline ──
599
+
600
+ #[test]
601
+ fn fix_a7_empty_body_inline_falls_back_to_role() {
602
+ let role = "---\nname: w\nrole: Reviewer Role\nprovider: codex\nauth_mode: subscription\ntools:\n - mcp_team\n---\n";
603
+ let team = build_team(TM_CODEX, &[("w.md", role)], &[]);
604
+ let spec = compile_team(&team).unwrap();
605
+ assert_eq!(str_path(agent0(&spec), &["system_prompt", "inline"]), "Reviewer Role");
606
+ }
607
+
608
+ // ── A8 auth_mode ──
609
+
610
+ #[test]
611
+ fn fix_a8_official_api_without_profile_errors() {
612
+ // official_api (any non-subscription) without profile MUST NOT silently compile.
613
+ let role = "---\nname: w\nrole: R\nprovider: codex\nauth_mode: official_api\ntools:\n - mcp_team\n---\nb\n";
614
+ let team = build_team(TM_CODEX, &[("w.md", role)], &[]);
615
+ let err = compile_team(&team).unwrap_err();
616
+ assert!(
617
+ err.to_string().contains("profile is required when auth_mode is 'official_api'"),
618
+ "got: {err}"
619
+ );
620
+ }
621
+
622
+ #[test]
623
+ fn fix_a8_compatible_api_error_names_the_auth_mode() {
624
+ // Full message form (the old test only checked the "profile is required" prefix).
625
+ let team = build_team(TEAM_BASE, &[("implementer.md", ROLE_COMPATIBLE_NO_PROFILE)], &[]);
626
+ let err = compile_team(&team).unwrap_err();
627
+ assert!(
628
+ err.to_string().contains("profile is required when auth_mode is 'compatible_api'"),
629
+ "got: {err}"
630
+ );
631
+ }
632
+
633
+ // ── A9 bool/int coercion (Python bool()/int() over the simple_yaml value) ──
634
+
635
+ #[test]
636
+ fn fix_a9_bool_coercion_yes_and_one_are_true() {
637
+ for v in ["yes", "1"] {
638
+ let tm = format!("---\nname: T\nprovider: codex\ndangerous_auto_approve: {v}\nworker_to_worker: {v}\n---\nx\n");
639
+ let team = build_team(&tm, &[("w.md", &role_nomodel("codex"))], &[]);
640
+ let spec = compile_team(&team).unwrap();
641
+ assert_eq!(get_path(&spec, &["runtime", "dangerous_auto_approve"]), Some(&Value::Bool(true)), "value {v}");
642
+ assert_eq!(get_path(&spec, &["communication", "worker_to_worker"]), Some(&Value::Bool(true)), "value {v}");
643
+ }
644
+ }
645
+
646
+ #[test]
647
+ fn fix_a9_bool_coercion_no_is_python_truthy() {
648
+ // FLAG: Python bool("no") is TRUE (only 0/false/False are falsy). The contract
649
+ // note said "no->false" but Python golden is no->true; we lock Python.
650
+ let tm = "---\nname: T\nprovider: codex\ndangerous_auto_approve: no\n---\nx\n";
651
+ let team = build_team(tm, &[("w.md", &role_nomodel("codex"))], &[]);
652
+ let spec = compile_team(&team).unwrap();
653
+ assert_eq!(get_path(&spec, &["runtime", "dangerous_auto_approve"]), Some(&Value::Bool(true)));
654
+ }
655
+
656
+ #[test]
657
+ fn fix_a9_int_coercion_of_quoted_string() {
658
+ // tick_interval_sec: "5" (a YAML string) → int("5") = 5.
659
+ let tm = "---\nname: T\nprovider: codex\ntick_interval_sec: \"5\"\n---\nx\n";
660
+ let team = build_team(tm, &[("w.md", &role_nomodel("codex"))], &[]);
661
+ let spec = compile_team(&team).unwrap();
662
+ assert_eq!(get_path(&spec, &["runtime", "tick_interval_sec"]), Some(&Value::Int(5)));
663
+ }
664
+
665
+ // ── A10 CRLF normalization ──
666
+
667
+ #[test]
668
+ fn fix_a10_crlf_role_doc_front_matter_is_parsed() {
669
+ // Windows-authored \r\n doc: read_text universal-newlines normalizes before
670
+ // the "---\n" check, so the front matter parses (current keeps \r\n → no FM).
671
+ let crlf_role = "---\r\nname: crlfworker\r\nrole: R\r\nprovider: codex\r\nauth_mode: subscription\r\ntools:\r\n - mcp_team\r\n---\r\n\r\nbody\r\n";
672
+ let team = build_team(TM_CODEX, &[("w.md", crlf_role)], &[]);
673
+ let spec = compile_team(&team).expect("CRLF role doc must parse its front matter");
674
+ assert_eq!(agent0(&spec).get("id").and_then(Value::as_str), Some("crlfworker"));
675
+ assert_eq!(str_path(agent0(&spec), &["system_prompt", "inline"]), "body");
676
+ }
677
+
678
+ // ── A11 error-message paths ──
679
+
680
+ #[test]
681
+ fn fix_a11_missing_team_md_message_includes_team_md_path() {
682
+ let team = build_layout("p", "teamdir", None, AgentsDir::WithRole(&role_nomodel("codex")));
683
+ let err = compile_team(&team).unwrap_err();
684
+ assert!(err.to_string().contains("/TEAM.md: missing TEAM.md"), "got: {err}");
685
+ }
686
+
687
+ #[test]
688
+ fn fix_a11_missing_agents_dir_message_includes_agents_path() {
689
+ let team = build_layout("p", "teamdir", Some(TM_CODEX), AgentsDir::Missing);
690
+ let err = compile_team(&team).unwrap_err();
691
+ assert!(err.to_string().contains("/agents: missing agents directory"), "got: {err}");
692
+ }
693
+
694
+ #[test]
695
+ fn fix_a11_agents_is_a_file_reports_no_role_docs() {
696
+ // Python uses exists() (not is_dir()): agents-as-a-file passes the existence
697
+ // gate, then the empty glob → "no role docs found" (path = agents dir).
698
+ let team = build_layout("p", "teamdir", Some(TM_CODEX), AgentsDir::File);
699
+ let err = compile_team(&team).unwrap_err();
700
+ assert!(err.to_string().contains("/agents: no role docs found"), "got: {err}");
701
+ }