@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,933 @@
1
+ use super::*;
2
+ use crate::transport::test_support::OfflineTransport;
3
+ use serial_test::serial;
4
+
5
+ const QS_TEAM_MD: &str =
6
+ "---\nname: quickteam\nobjective: Quick start.\nprovider: codex\n---\n\nQuick-start team.\n";
7
+ pub(super) const QS_VALID_ROLE: &str = "---\nname: implementer\nrole: Implementation Engineer\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nImplement bounded tasks.\n";
8
+ const QS_ROLE_NO_PROVIDER: &str = "---\nname: implementer\nrole: Implementation Engineer\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nImplement bounded tasks.\n";
9
+
10
+ /// A REAL team dir `<base>/teamdir/{TEAM.md, agents/implementer.md}` — already in team-dir
11
+ /// layout, so quick_start's prepare_quick_start_team returns it as-is and writes the compiled
12
+ /// `team.spec.yaml` into it (diagnose/quick_start.py:prepare_quick_start_team, before launch).
13
+ pub(super) fn quick_start_team_dir(role_doc: &str) -> PathBuf {
14
+ let team = temp_ws().join("teamdir");
15
+ std::fs::create_dir_all(team.join("agents")).unwrap();
16
+ std::fs::write(team.join("TEAM.md"), QS_TEAM_MD).unwrap();
17
+ std::fs::write(team.join("agents").join("implementer.md"), role_doc).unwrap();
18
+ team
19
+ }
20
+
21
+ /// A no-owner running workspace: state.json carries session_name + agents but NO team_owner →
22
+ /// check_team_owner returns None = allowed (owner_gate.rs:48), so the owner-gated entry points can
23
+ /// proceed PAST the owner gate. Also inits the real team.db.
24
+ fn unowned_running_ws() -> PathBuf {
25
+ let ws = temp_ws();
26
+ crate::state::persist::save_runtime_state(
27
+ &ws,
28
+ &json!({
29
+ "session_name": "team-x",
30
+ "agents": { "w1": { "provider": "codex", "role": "Worker", "model": "gpt-5.5", "auth_mode": "subscription" } },
31
+ }),
32
+ )
33
+ .unwrap();
34
+ let _ = crate::message_store::MessageStore::open(&ws).unwrap();
35
+ ws
36
+ }
37
+
38
+ // P0 — quick_start over a VALID team dir must COMPILE the real spec (the compiler runs before any
39
+ // launch/spawn) → team.spec.yaml is written carrying the agent set, and the result is NOT the
40
+ // hardcoded "no role docs found" PreflightBlocked. Golden: quick_start.py → _compile_team_dir_spec
41
+ // writes spec_path=team_dir/team.spec.yaml BEFORE launch.
42
+ #[test]
43
+ fn quick_start_compiles_real_spec_to_team_spec_yaml() {
44
+ let team = quick_start_team_dir(QS_VALID_ROLE);
45
+ let transport = OfflineTransport::new();
46
+ let result = quick_start_with_transport(&team, None, true, true, None, &transport);
47
+
48
+ // OBSERVABLE 1 (real compiler ran; spawn-independent): the compiled spec is written.
49
+ let spec_path = team.join("team.spec.yaml");
50
+ assert!(
51
+ spec_path.exists(),
52
+ "quick_start must compile the team dir and write team.spec.yaml (the real compiler runs \
53
+ before launch); the stub returns before compiling. result={result:?}"
54
+ );
55
+ let spec_text = std::fs::read_to_string(&spec_path).unwrap_or_default();
56
+ assert!(
57
+ spec_text.contains("implementer"),
58
+ "the compiled team.spec.yaml must carry the role-doc agent 'implementer'"
59
+ );
60
+
61
+ // OBSERVABLE 2 (report, robust): a VALID dir must never be the hardcoded always-blocked stub.
62
+ if let Ok(QuickStartReport::PreflightBlocked { blockers, .. }) = &result {
63
+ assert!(
64
+ !blockers.iter().any(|b| b.contains("no role docs found")),
65
+ "a VALID team dir must never yield the hardcoded 'no role docs found' blocker"
66
+ );
67
+ }
68
+ }
69
+
70
+ // P0 — quick_start over an INVALID role doc (missing `provider`) must surface the REAL compile
71
+ // error, distinct from the stub's hardcoded "no role docs found". Golden: compile_team raises
72
+ // "missing front matter field provider" (compiler.py:_validate_role_doc), before preflight.
73
+ #[test]
74
+ fn quick_start_invalid_role_doc_surfaces_real_compile_error() {
75
+ let team = quick_start_team_dir(QS_ROLE_NO_PROVIDER);
76
+ let transport = OfflineTransport::new();
77
+ let result = quick_start_with_transport(&team, None, true, true, None, &transport);
78
+
79
+ let text = format!("{result:?}");
80
+ assert!(
81
+ text.contains("provider"),
82
+ "an invalid role doc (missing provider) must surface the real compile error mentioning \
83
+ 'provider'; got {text}"
84
+ );
85
+ assert!(
86
+ !text.contains("no role docs found"),
87
+ "must NOT be the hardcoded stub blocker — real preflight/compile distinguishes the cause"
88
+ );
89
+ }
90
+
91
+ // P0 — launch (dry_run) over a real compiled spec must resolve the REAL route/permission plan
92
+ // (no spawn) — NOT the hardcoded RequirementUnmet stub. Golden: launch/core.py dry_run resolves
93
+ // routing + permissions without starting any process.
94
+ #[test]
95
+ fn launch_dry_run_resolves_real_plan_not_stub_error() {
96
+ let team = quick_start_team_dir(QS_VALID_ROLE);
97
+ let spec = crate::compiler::compile_team(&team).expect("compile the seeded valid team");
98
+ let spec_path = team.join("team.spec.yaml");
99
+ std::fs::write(&spec_path, crate::model::yaml::dumps(&spec)).unwrap();
100
+
101
+ let result = launch(&spec_path, true, true, true);
102
+
103
+ match result {
104
+ Err(LifecycleError::RequirementUnmet(msg)) if msg.contains("not available") => {
105
+ panic!("launch returned the hardcoded stub error; the real dry_run plan was never resolved");
106
+ }
107
+ Ok(report) => {
108
+ assert!(report.dry_run, "dry_run launch must report dry_run=true");
109
+ assert!(
110
+ !report.routes.is_empty() || !report.permissions.is_empty(),
111
+ "dry_run launch must resolve a real route/permission plan from the compiled spec"
112
+ );
113
+ }
114
+ // any OTHER Err (a real requirement/transport error) still proves the stub is gone.
115
+ Err(_) => {}
116
+ }
117
+ }
118
+
119
+ // P1 — start_agent must PROCEED past the owner gate for a no-owner workspace (check_team_owner →
120
+ // None = allowed). The stub hard-returns OwnerRefused unconditionally. Golden: lifecycle/start.py
121
+ // runs under the owner gate, then the resume-or-fresh decision. (The resume/fresh PLAN + real
122
+ // spawn is the #[ignore] OS boundary — see quick_start_full_ready_real_spawn.)
123
+ #[test]
124
+ fn start_agent_proceeds_past_owner_gate_for_unowned_workspace() {
125
+ let ws = unowned_running_ws();
126
+ let transport = OfflineTransport::new();
127
+ let result = start_agent_with_transport(&ws, &AgentId::new("w1"), false, false, true, None, &transport);
128
+ assert!(
129
+ !matches!(result, Err(LifecycleError::OwnerRefused(_))),
130
+ "start_agent must reach the real chain for a no-owner workspace, not hard-refuse owner; got {result:?}"
131
+ );
132
+ }
133
+
134
+ // P1 — restart (Route B) must PROCEED past the owner gate for a no-owner workspace. Stub →
135
+ // OwnerRefused. Golden: restart/orchestration.py runs the resume-selection under the owner gate.
136
+ #[test]
137
+ fn restart_proceeds_past_owner_gate_for_unowned_workspace() {
138
+ let ws = unowned_running_ws();
139
+ let transport = OfflineTransport::new();
140
+ let result = restart_with_transport(&ws, true, None, &transport);
141
+ assert!(
142
+ !matches!(result, Err(LifecycleError::OwnerRefused(_))),
143
+ "restart must reach the real Route-B chain for a no-owner workspace; got {result:?}"
144
+ );
145
+ }
146
+
147
+ // P1 — add_agent must PROCEED past the owner gate (no-owner ws) and reach the real recompile chain.
148
+ // Stub → OwnerRefused. Golden: lifecycle/operations.py:add_agent recompiles the role doc into the
149
+ // spec under the owner gate.
150
+ #[test]
151
+ fn add_agent_proceeds_past_owner_gate_and_reaches_recompile() {
152
+ let ws = unowned_running_ws();
153
+ let role = ws.join("w2-role.md");
154
+ std::fs::write(
155
+ &role,
156
+ "---\nname: w2\nrole: Second Worker\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nSecond worker.\n",
157
+ )
158
+ .unwrap();
159
+ let transport = OfflineTransport::new();
160
+ let result = add_agent_with_transport(&ws, &AgentId::new("w2"), &role, false, None, &transport);
161
+ assert!(
162
+ !matches!(result, Err(LifecycleError::OwnerRefused(_))),
163
+ "add_agent must reach the real recompile chain for a no-owner workspace; got {result:?}"
164
+ );
165
+ }
166
+
167
+ // P1 — fork_agent must PROCEED past the owner gate (no-owner ws). Stub → OwnerRefused.
168
+ // Golden: lifecycle/operations.py:fork_agent native session fork under the owner gate.
169
+ #[test]
170
+ fn fork_agent_proceeds_past_owner_gate_for_unowned_workspace() {
171
+ let ws = unowned_running_ws();
172
+ let transport = OfflineTransport::new();
173
+ let result = fork_agent_with_transport(
174
+ &ws,
175
+ &AgentId::new("w1"),
176
+ &AgentId::new("w1-fork"),
177
+ false,
178
+ None,
179
+ &transport,
180
+ );
181
+ assert!(
182
+ !matches!(result, Err(LifecycleError::OwnerRefused(_))),
183
+ "fork_agent must reach the real native-fork chain for a no-owner workspace; got {result:?}"
184
+ );
185
+ }
186
+
187
+ // REAL-MACHINE boundary (acceptance framework): a full quick_start that actually LAUNCHES workers
188
+ // needs a live tmux + provider spawn — db/state seeding + the Ready report happen INSIDE launch()'s
189
+ // real spawn. Asserted by the acceptance crate, not here (do NOT fake the spawn).
190
+ #[test]
191
+ #[ignore = "real-machine: full quick_start Ready needs a live tmux + provider spawn (db/state seeding \
192
+ happens inside launch's real spawn). Acceptance-crate boundary, not a unit RED."]
193
+ fn quick_start_full_ready_real_spawn() {
194
+ let team = quick_start_team_dir(QS_VALID_ROLE);
195
+ let report = quick_start(&team, None, true, true, None).expect("quick_start launches the team");
196
+ assert!(matches!(report, QuickStartReport::Ready { .. }), "got {report:?}");
197
+ }
198
+
199
+ // ═════════════════════════════════════════════════════════════════════════
200
+ // SPINE-WIRING (③ review→fix) RED — lifecycle wiring divergences vs golden v0.2.11
201
+ // (diagnose/quick_start.py, launch/core.py + config.py + routing.py, restart/orchestration.py,
202
+ // lifecycle/operations.py). /tmp/spine_divergences.md #8/#15, #10, #12, #13, #14.
203
+ // ═════════════════════════════════════════════════════════════════════════
204
+
205
+ const QS_TEAM_MD_DANGEROUS: &str =
206
+ "---\nname: dangerteam\nobjective: Dangerous.\nprovider: codex\ndangerous_auto_approve: true\n---\n\nteam.\n";
207
+
208
+ fn quick_start_team_dir_custom(team_md: &str, role_doc: &str) -> PathBuf {
209
+ let team = temp_ws().join("teamdir");
210
+ std::fs::create_dir_all(team.join("agents")).unwrap();
211
+ std::fs::write(team.join("TEAM.md"), team_md).unwrap();
212
+ std::fs::write(team.join("agents").join("implementer.md"), role_doc).unwrap();
213
+ team
214
+ }
215
+
216
+ fn compiled_spec_path(team: &std::path::Path) -> PathBuf {
217
+ let spec = crate::compiler::compile_team(team).expect("compile team");
218
+ let spec_path = team.join("team.spec.yaml");
219
+ std::fs::write(&spec_path, crate::model::yaml::dumps(&spec)).unwrap();
220
+ spec_path
221
+ }
222
+
223
+ fn raw_runtime_state(workspace: &std::path::Path) -> (String, serde_json::Value) {
224
+ let state_path = crate::model::paths::runtime_dir(workspace).join("state.json");
225
+ let raw = std::fs::read_to_string(&state_path)
226
+ .unwrap_or_else(|e| panic!("read state.json at {}: {e}", state_path.display()));
227
+ let state = serde_json::from_str(&raw).expect("state.json must parse");
228
+ (raw, state)
229
+ }
230
+
231
+ struct EnvVarGuard {
232
+ key: &'static str,
233
+ previous: Option<String>,
234
+ }
235
+
236
+ impl EnvVarGuard {
237
+ fn set(key: &'static str, value: &str) -> Self {
238
+ let previous = std::env::var(key).ok();
239
+ unsafe {
240
+ std::env::set_var(key, value);
241
+ }
242
+ Self { key, previous }
243
+ }
244
+
245
+ fn unset(key: &'static str) -> Self {
246
+ let previous = std::env::var(key).ok();
247
+ unsafe {
248
+ std::env::remove_var(key);
249
+ }
250
+ Self { key, previous }
251
+ }
252
+ }
253
+
254
+ impl Drop for EnvVarGuard {
255
+ fn drop(&mut self) {
256
+ unsafe {
257
+ if let Some(value) = self.previous.take() {
258
+ std::env::set_var(self.key, value);
259
+ } else {
260
+ std::env::remove_var(self.key);
261
+ }
262
+ }
263
+ }
264
+ }
265
+
266
+ fn compiled_spec_path_with_paused_agent(team: &std::path::Path) -> PathBuf {
267
+ let mut spec = crate::compiler::compile_team(team).expect("compile team");
268
+ let crate::model::yaml::Value::Map(root) = &mut spec else {
269
+ panic!("compiled spec must be a map");
270
+ };
271
+ let agents = root
272
+ .iter_mut()
273
+ .find(|(key, _)| key == "agents")
274
+ .and_then(|(_, value)| match value {
275
+ crate::model::yaml::Value::List(agents) => Some(agents),
276
+ _ => None,
277
+ })
278
+ .expect("compiled spec must contain agents");
279
+ let first = agents.first_mut().expect("compiled spec must contain an agent");
280
+ let crate::model::yaml::Value::Map(agent) = first else {
281
+ panic!("compiled agent must be a map");
282
+ };
283
+ agent.push(("paused".to_string(), crate::model::yaml::Value::Bool(true)));
284
+ let spec_path = team.join("team.spec.yaml");
285
+ std::fs::write(&spec_path, crate::model::yaml::dumps(&spec)).unwrap();
286
+ spec_path
287
+ }
288
+
289
+ fn unowned_running_ws_all_paused() -> PathBuf {
290
+ let ws = temp_ws();
291
+ crate::state::persist::save_runtime_state(
292
+ &ws,
293
+ &json!({
294
+ "session_name": "team-x",
295
+ "agents": { "w1": { "provider": "codex", "role": "Worker", "model": "gpt-5.5", "auth_mode": "subscription", "status": "paused" } },
296
+ }),
297
+ )
298
+ .unwrap();
299
+ let _ = crate::message_store::MessageStore::open(&ws).unwrap();
300
+ ws
301
+ }
302
+
303
+ // #15 — quick_start seeds runtime state under team_workspace(team_dir) (the PARENT), not inside the
304
+ // team dir, so restart/status can locate it. Golden quick_start.py:35 team_workspace(team_dir).
305
+ #[test]
306
+ fn spine_quick_start_seeds_state_under_team_workspace_not_team_dir() {
307
+ let team = quick_start_team_dir(QS_VALID_ROLE); // <base>/teamdir
308
+ let transport = OfflineTransport::new();
309
+ let _ = quick_start_with_transport(&team, None, true, true, None, &transport);
310
+ let workspace = team.parent().unwrap(); // team_workspace(<base>/teamdir) = <base>
311
+ let ws_state = crate::model::paths::runtime_dir(workspace).join("state.json");
312
+ assert!(
313
+ ws_state.exists(),
314
+ "quick_start must seed runtime state under team_workspace(team_dir)={} (not inside the team dir)",
315
+ workspace.display()
316
+ );
317
+ }
318
+
319
+ // #10 — launch dry-run safety is derived from spec.runtime.dangerous_auto_approve (source=runtime_config),
320
+ // NOT a hardcoded Disabled. Golden config.py:effective_runtime_config.
321
+ #[test]
322
+ fn spine_launch_dry_run_safety_reflects_spec_dangerous() {
323
+ let team = quick_start_team_dir_custom(QS_TEAM_MD_DANGEROUS, QS_VALID_ROLE);
324
+ let spec_path = compiled_spec_path(&team);
325
+ let report = launch(&spec_path, true, false, true).expect("dry_run launch");
326
+ assert!(report.safety.enabled, "safety must reflect spec.runtime.dangerous_auto_approve=true");
327
+ assert_eq!(
328
+ report.safety.source,
329
+ DangerousApprovalSource::RuntimeConfig,
330
+ "safety source must be runtime_config when spec-declared"
331
+ );
332
+ }
333
+
334
+ // #12 — launch dry-run produces one RoutingDecision PER spec.task via route_task, with the real task id
335
+ // and the route_task reason taxonomy — not one synthetic 'default_assignee' route. Golden core.py:77-88.
336
+ #[test]
337
+ fn spine_launch_dry_run_routes_one_per_task_with_route_reason() {
338
+ let team = quick_start_team_dir(QS_VALID_ROLE);
339
+ let spec_path = compiled_spec_path(&team);
340
+ let report = launch(&spec_path, true, true, true).expect("dry_run launch");
341
+ // compile_team emits one task 'task_initial' assigned to 'implementer' → reason 'explicit assignee on task'.
342
+ let route = report
343
+ .routes
344
+ .iter()
345
+ .find(|r| r.task_id.as_deref() == Some("task_initial"))
346
+ .unwrap_or_else(|| panic!("a route for task_initial expected; got {:?}", report.routes));
347
+ assert_eq!(
348
+ route.reason, "explicit assignee on task",
349
+ "the route reason must come from route_task, not the hardcoded 'default_assignee'"
350
+ );
351
+ assert_eq!(route.selected_agent.as_str(), "implementer");
352
+ }
353
+
354
+ // #13 — an empty/all-paused restart decision set is NOT an atomic refusal (remove the empty-decisions
355
+ // clause); only a non-empty unresumable set (and !allow_fresh) refuses. Golden orchestration.py.
356
+ #[test]
357
+ fn spine_restart_empty_team_is_not_atomic_refusal() {
358
+ let ws = unowned_running_ws_all_paused();
359
+ let transport = OfflineTransport::new();
360
+ let result = restart_with_transport(&ws, true, None, &transport);
361
+ assert!(
362
+ !matches!(result, Ok(RestartReport::RefusedResumeAtomicity { .. })),
363
+ "an all-paused/empty team must NOT yield an atomic refusal (empty decision set is not a refusal); got {result:?}"
364
+ );
365
+ }
366
+
367
+ // #14 — add_agent rejects a duplicate agent id (golden operations.py:301 'agent id already exists'),
368
+ // BEFORE any full recompile. Current does a full compile_team with no duplicate-id guard.
369
+ #[test]
370
+ fn spine_add_agent_rejects_duplicate_agent_id() {
371
+ let team = quick_start_team_dir(QS_VALID_ROLE); // team already has agent 'implementer'
372
+ let role = team.join("dup-role.md");
373
+ std::fs::write(&role, QS_VALID_ROLE).unwrap(); // a role doc named 'implementer' = duplicate
374
+ let transport = OfflineTransport::new();
375
+ let result = add_agent_with_transport(&team, &AgentId::new("implementer"), &role, false, None, &transport);
376
+ let text = format!("{result:?}");
377
+ assert!(
378
+ text.contains("already exists"),
379
+ "add_agent must reject a duplicate agent id (golden 'agent id already exists'); got {text}"
380
+ );
381
+ }
382
+
383
+ // ═════════════════════════════════════════════════════════════════════════
384
+ // SPAWN sub-phase RED — launch(dry_run=false) must REALLY spawn (unlocks the acceptance
385
+ // framework's cheap real-machine Tier-1). Golden launch/core.py: create the session + one worker
386
+ // window per agent running its provider command (with workspace + agent-id context), populate the
387
+ // started list, attach the leader receiver. Today launch(dry_run=false) is the hardcoded stub
388
+ // RequirementUnmet("launch spawn requires live transport/provider boundary").
389
+ // ═════════════════════════════════════════════════════════════════════════
390
+
391
+ // P0 — launch(dry_run=false) over a real compiled spec must run the real spawn chain (create session +
392
+ // spawn ≥1 worker, populating LaunchReport.started), NOT the hardcoded spawn stub. The recording-
393
+ // transport spawn-CALL assertion (spawn_first/spawn_into argv) needs a transport-injection seam in
394
+ // launch — flagged to the leader (launch takes no transport today); the real tmux new-session is the
395
+ // #[ignore] acceptance boundary (see quick_start_full_ready_real_spawn). Here we assert the seam-
396
+ // independent observable: the spawn stub is gone + LaunchReport.started is populated + dry_run=false.
397
+ //
398
+ // CONVERTED to #[ignore] (final real-daemon sub-phase): the porter wires PUBLIC launch() to the real
399
+ // TmuxBackend (today it uses NoopLaunchTransport), so once wired this test spawns REAL tmux — a unit
400
+ // test must not. The OS-edge-mocked spawn-orchestration coverage moved to
401
+ // `launch_with_transport_records_one_spawn_per_agent_carrying_build_command` (recording mock transport).
402
+ #[test]
403
+ #[ignore = "real-machine: public launch(dry_run=false) spawns real tmux once wired to TmuxBackend; \
404
+ in-process coverage is launch_with_transport_records_one_spawn_per_agent_carrying_build_command"]
405
+ fn spawn_launch_not_dry_run_is_no_longer_the_spawn_stub() {
406
+ let team = quick_start_team_dir(QS_VALID_ROLE);
407
+ let spec_path = compiled_spec_path(&team);
408
+
409
+ let result = launch(&spec_path, false, true, true);
410
+
411
+ match result {
412
+ Err(LifecycleError::RequirementUnmet(msg)) if msg.contains("requires live transport") => {
413
+ panic!(
414
+ "launch(dry_run=false) still returns the hardcoded spawn stub; the real spawn was never wired: {msg}"
415
+ );
416
+ }
417
+ Ok(report) => {
418
+ assert!(!report.dry_run, "a real launch must report dry_run=false");
419
+ assert!(
420
+ !report.started.is_empty(),
421
+ "launch(dry_run=false) must spawn >=1 worker and populate LaunchReport.started; got {:?}",
422
+ report.started
423
+ );
424
+ }
425
+ // any OTHER Err (a real transport/preflight error from the wired spawn) still proves the stub is gone.
426
+ Err(_) => {}
427
+ }
428
+ }
429
+
430
+ // ═════════════════════════════════════════════════════════════════════════
431
+ // (C) public launch → real TmuxBackend. The OS edge is mocked by a RECORDING transport via the
432
+ // launch_with_transport seam; the REAL TmuxBackend swap is exercised by the acceptance framework
433
+ // (#[ignore] spawn_launch_not_dry_run_is_no_longer_the_spawn_stub). Golden launch/core.py.
434
+ // ═════════════════════════════════════════════════════════════════════════
435
+
436
+ // (C) — launch_with_transport(dry_run=false, <recording transport>) creates one spawn per compiled
437
+ // agent, the recorded spawn argv carries that agent's provider build_command, and LaunchReport.started
438
+ // lists them. NOTE: GREEN today — spawn_agents already drives the injected transport; this LOCKS the
439
+ // spawn orchestration contract (replacing the now-#[ignore]'d public-launch test's in-process coverage).
440
+ // The genuine remaining gap — public launch() must construct a real TmuxBackend (today NoopLaunchTransport)
441
+ // — is not cleanly assertable in-process without spawning; it rides the porter's wiring + the #[ignore]
442
+ // real-machine test above.
443
+ #[test]
444
+ fn launch_with_transport_records_one_spawn_per_agent_carrying_build_command() {
445
+ let team = quick_start_team_dir(QS_VALID_ROLE); // one agent: implementer / provider codex
446
+ let spec_path = compiled_spec_path(&team);
447
+ let transport = OfflineTransport::new();
448
+
449
+ let report = launch_with_transport(&spec_path, false, true, true, &transport)
450
+ .expect("launch_with_transport must spawn against the recording transport");
451
+
452
+ let recorded = transport.spawn_records();
453
+ assert_eq!(
454
+ recorded.len(),
455
+ report.started.len(),
456
+ "exactly one spawn per started agent; recorded={recorded:?} started={:?}",
457
+ report.started
458
+ );
459
+ assert!(!report.started.is_empty(), "a real (non-dry-run) launch must spawn >=1 worker");
460
+ assert!(!report.dry_run, "launch_with_transport(dry_run=false) must report dry_run=false");
461
+ assert_eq!(recorded[0].0, "spawn_first", "the first worker uses new-session (spawn_first)");
462
+ assert!(
463
+ recorded.iter().any(|(_, argv)| argv.iter().any(|a| a == "codex")),
464
+ "each spawn argv must carry the agent's provider build_command (codex); got {recorded:?}"
465
+ );
466
+ assert!(
467
+ report.started.iter().any(|s| s.agent_id.as_str() == "implementer"),
468
+ "LaunchReport.started must list the compiled agent; got {:?}",
469
+ report.started
470
+ );
471
+ }
472
+
473
+ // ═════════════════════════════════════════════════════════════════════════
474
+ // rt-host-a REGRESSION — `team-agent quick-start` compiles+seeds but NEVER spawns workers:
475
+ // quick_start_with_transport hardcodes launch dry_run=TRUE, so launch takes the started=Vec::new()
476
+ // branch and spawn_agents is never called. The 788-green missed it (no test drove quick_start through
477
+ // the spawn path). Golden: a real quick-start must spawn one worker per compiled agent into the session.
478
+ // ═════════════════════════════════════════════════════════════════════════
479
+
480
+ /// Seed a HEALTHY coordinator at `workspace` (this process's pid + matching metadata + db schema) so
481
+ /// quick_start's internal start_coordinator returns AlreadyRunning — NO real daemon subprocess spawn.
482
+ pub(super) fn seed_healthy_coordinator(workspace: &std::path::Path) {
483
+ let wp = crate::coordinator::WorkspacePath::new(workspace.to_path_buf());
484
+ std::fs::create_dir_all(crate::model::paths::runtime_dir(workspace)).unwrap();
485
+ let _ = crate::message_store::MessageStore::open(workspace).unwrap(); // create db schema (schema.ok)
486
+ let me = crate::coordinator::Pid::new(std::process::id());
487
+ crate::coordinator::write_coordinator_metadata(&wp, me, crate::coordinator::MetadataSource::Boot).unwrap();
488
+ std::fs::write(crate::coordinator::coordinator_pid_path(&wp), me.to_string()).unwrap();
489
+ }
490
+
491
+ // RED — quick_start_with_transport must drive the REAL spawn path: launch.dry_run==false, started
492
+ // non-empty (one per compiled agent), and the transport records >=1 spawn carrying that agent's
493
+ // provider build_command. Today the dry_run=true bug -> launch.started empty + ZERO spawns recorded ->
494
+ // RED at assertion (NOT a panic). OS-safe: recording transport (no real tmux) + seeded-healthy
495
+ // coordinator (start_coordinator AlreadyRunning -> no daemon subprocess).
496
+ #[test]
497
+ fn quick_start_with_transport_spawns_workers_not_dry_run() {
498
+ let team = quick_start_team_dir(QS_VALID_ROLE); // one agent: implementer / provider codex
499
+ let workspace = team.parent().expect("team_workspace(team_dir) = parent"); // where start_coordinator runs
500
+ seed_healthy_coordinator(workspace);
501
+ let transport = OfflineTransport::new();
502
+
503
+ let report = quick_start_with_transport(&team, None, true, true, None, &transport)
504
+ .expect("quick_start_with_transport must reach a report");
505
+
506
+ let launch = match report {
507
+ QuickStartReport::Ready { launch, .. } => *launch,
508
+ other => panic!("quick_start must reach Ready (the spawn path); got {other:?}"),
509
+ };
510
+ // (1) the rt-host-a bug: launch dry_run hardcoded true -> this is the load-bearing regression assert.
511
+ assert!(
512
+ !launch.dry_run,
513
+ "quick_start must SPAWN workers (launch.dry_run == false); the rt-host-a bug hardcodes dry_run=true \
514
+ so launch takes the empty started branch and never spawns"
515
+ );
516
+ // (2) one started entry per compiled agent (the dry-run branch leaves this empty).
517
+ assert!(
518
+ !launch.started.is_empty(),
519
+ "quick_start must populate launch.started (>=1 worker spawned), not the dry-run empty Vec; got {:?}",
520
+ launch.started
521
+ );
522
+ assert!(
523
+ launch.started.iter().any(|s| s.agent_id.as_str() == "implementer"),
524
+ "launch.started must list the compiled agent 'implementer'; got {:?}",
525
+ launch.started
526
+ );
527
+ // (3) the transport actually recorded the spawn, carrying the provider build_command.
528
+ let recorded = transport.spawn_records();
529
+ assert!(
530
+ !recorded.is_empty(),
531
+ "quick_start must drive the transport spawn path (>=1 spawn recorded); the dry-run bug records ZERO spawns"
532
+ );
533
+ assert_eq!(recorded[0].0, "spawn_first", "the first worker uses new-session (spawn_first); got {recorded:?}");
534
+ assert!(
535
+ recorded.iter().any(|(_, argv)| argv.iter().any(|a| a == "codex")),
536
+ "the spawn argv must carry the agent's provider build_command (codex); got {recorded:?}"
537
+ );
538
+ }
539
+
540
+ // REAL-MACHINE residency boundary (acceptance framework): the PUBLIC quick_start (real TmuxBackend +
541
+ // real start_coordinator) on a fresh ws must leave a LIVE tmux session AND a ps-verifiable resident
542
+ // coordinator daemon (start_coordinator -> live pid). The framework verifies residency via ps; here we
543
+ // only assert the in-report spawn observable. #[ignore] — spawns real tmux + a real daemon subprocess.
544
+ #[test]
545
+ #[ignore = "real-machine: live tmux session + resident coordinator daemon (framework verifies via ps)"]
546
+ fn quick_start_fresh_ws_spawns_resident_tmux_and_coordinator() {
547
+ let team = quick_start_team_dir(QS_VALID_ROLE);
548
+ let report = quick_start(&team, None, true, true, None).expect("quick_start");
549
+ match report {
550
+ QuickStartReport::Ready { launch, .. } => {
551
+ assert!(!launch.dry_run, "a real quick_start must spawn (not dry-run)");
552
+ assert!(
553
+ !launch.started.is_empty(),
554
+ "a real quick_start must spawn >=1 worker into a live tmux session; got {:?}",
555
+ launch.started
556
+ );
557
+ }
558
+ other => panic!("a fresh quick_start must reach Ready; got {other:?}"),
559
+ }
560
+ }
561
+
562
+ // ═════════════════════════════════════════════════════════════════════════
563
+ // rt-host-a LOOP #2 — cli-handler DELEGATION sweep (same-class stub): restart / start_agent /
564
+ // add_agent return RequirementUnmet at the spawn boundary and NEVER drive the transport (zero spawns)
565
+ // nor start the coordinator. RED via the new *_with_transport seams + a RecordingTransport. OS-safe:
566
+ // recording transport (no real tmux) + seeded-healthy-coordinator (start_coordinator AlreadyRunning).
567
+ // Golden: restart/orchestration.py (Route-B resume spawn), lifecycle/start.py, lifecycle/operations.py.
568
+ // ═════════════════════════════════════════════════════════════════════════
569
+
570
+ pub(super) const DELEG_ROLE_ALPHA: &str = "---\nname: alpha\nrole: Alpha Worker\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nAlpha.\n";
571
+ pub(super) const DELEG_ROLE_BRAVO: &str = "---\nname: bravo\nrole: Bravo Worker\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nBravo.\n";
572
+ const DELEG_ROLE_WORKER2: &str = "---\nname: worker2\nrole: Second Worker\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nSecond worker.\n";
573
+
574
+ /// A workspace (= self-contained team dir) with a compiled 2-agent spec + state listing alpha/bravo as
575
+ /// RESUMABLE (running, valid first_send_at, session_id) + a seeded HEALTHY coordinator.
576
+ pub(super) fn restart_ws_two_resumable_workers() -> PathBuf {
577
+ let ws = temp_ws().join("restartteam");
578
+ std::fs::create_dir_all(ws.join("agents")).unwrap();
579
+ std::fs::write(ws.join("TEAM.md"), "---\nname: restartteam\nobjective: Restart probe.\nprovider: codex\n---\n\nteam.\n").unwrap();
580
+ std::fs::write(ws.join("agents").join("alpha.md"), DELEG_ROLE_ALPHA).unwrap();
581
+ std::fs::write(ws.join("agents").join("bravo.md"), DELEG_ROLE_BRAVO).unwrap();
582
+ let spec = crate::compiler::compile_team(&ws).expect("compile 2-agent team");
583
+ std::fs::write(ws.join("team.spec.yaml"), crate::model::yaml::dumps(&spec)).unwrap();
584
+ crate::state::persist::save_runtime_state(
585
+ &ws,
586
+ &json!({
587
+ "session_name": "team-restartteam",
588
+ "agents": {
589
+ "alpha": {"status": "running", "provider": "codex", "session_id": "sess-a", "first_send_at": "2026-05-27T10:00:00+00:00"},
590
+ "bravo": {"status": "running", "provider": "codex", "session_id": "sess-b", "first_send_at": "2026-05-27T10:00:00+00:00"}
591
+ }
592
+ }),
593
+ )
594
+ .unwrap();
595
+ seed_healthy_coordinator(&ws); // start_coordinator -> AlreadyRunning (no real daemon subprocess)
596
+ ws
597
+ }
598
+
599
+ // 2 [P0] — restart_with_transport must drive the REAL Route-B resume spawn: one spawn per resumable
600
+ // worker (first=spawn_first, rest=spawn_into), each carrying the provider build_command, + coordinator
601
+ // started. Today the stub returns RequirementUnmet with ZERO spawns -> RED at recorded.len().
602
+ #[test]
603
+ fn restart_with_transport_spawns_resumable_workers_not_stub() {
604
+ let ws = restart_ws_two_resumable_workers();
605
+ let transport = OfflineTransport::new();
606
+
607
+ let result = restart_with_transport(&ws, false, None, &transport);
608
+
609
+ let recorded = transport.spawn_records();
610
+ assert_eq!(
611
+ recorded.len(),
612
+ 2,
613
+ "restart must spawn ONE worker per resumable agent (alpha, bravo); the rt-host-a stub returns \
614
+ RequirementUnmet with ZERO spawns; got {recorded:?}"
615
+ );
616
+ assert_eq!(recorded[0].0, "spawn_first", "the first resumed worker uses new-session (spawn_first); got {recorded:?}");
617
+ assert!(
618
+ recorded.iter().any(|(kind, _)| kind == "spawn_into"),
619
+ "subsequent resumed workers use new-window (spawn_into); got {recorded:?}"
620
+ );
621
+ assert!(
622
+ recorded.iter().all(|(_, argv)| argv.iter().any(|a| a == "codex")),
623
+ "each resumed worker's spawn argv must carry the provider build_command (codex); got {recorded:?}"
624
+ );
625
+ assert!(
626
+ matches!(result, Ok(RestartReport::Restarted { coordinator_started: true, .. })),
627
+ "restart must reach RestartReport::Restarted with coordinator_started=true (AlreadyRunning, seeded); got {result:?}"
628
+ );
629
+ }
630
+
631
+ // 3 [P0] — start_agent_with_transport on a non-paused agent with a session_id must spawn EXACTLY ONE
632
+ // worker (resume) carrying the provider build_command. Today the stub returns RequirementUnmet with
633
+ // ZERO spawns -> RED at recorded.len().
634
+ #[test]
635
+ fn start_agent_with_transport_spawns_resume_not_stub() {
636
+ let ws = temp_ws().join("startagentws");
637
+ std::fs::create_dir_all(&ws).unwrap();
638
+ crate::state::persist::save_runtime_state(
639
+ &ws,
640
+ &json!({
641
+ "session_name": "team-sa",
642
+ "agents": {"alpha": {"status": "running", "provider": "codex", "session_id": "sess-a", "first_send_at": "2026-05-27T10:00:00+00:00"}}
643
+ }),
644
+ )
645
+ .unwrap();
646
+ seed_healthy_coordinator(&ws);
647
+ let transport = OfflineTransport::new();
648
+
649
+ let _result = start_agent_with_transport(&ws, &AgentId::new("alpha"), false, false, false, None, &transport);
650
+
651
+ let recorded = transport.spawn_records();
652
+ assert_eq!(
653
+ recorded.len(),
654
+ 1,
655
+ "start_agent must spawn EXACTLY ONE worker (resume); the rt-host-a stub returns RequirementUnmet \
656
+ with ZERO spawns; got {recorded:?}"
657
+ );
658
+ assert!(
659
+ recorded[0].1.iter().any(|a| a == "codex"),
660
+ "the resume spawn argv must carry the provider build_command (codex); got {:?}",
661
+ recorded[0].1
662
+ );
663
+ }
664
+
665
+ // 4 [P0] — add_agent_with_transport must (a) recompile + write the spec (already works) AND (b) spawn
666
+ // the new worker window. Today the stub recompiles then returns RequirementUnmet with ZERO spawns ->
667
+ // RED at (b). OS-safe: the new role file is OUTSIDE agents/ (so it's not a dup), seeded healthy coordinator.
668
+ #[test]
669
+ fn add_agent_with_transport_spawns_new_worker_not_stub() {
670
+ let team = temp_ws().join("addteam");
671
+ std::fs::create_dir_all(team.join("agents")).unwrap();
672
+ std::fs::write(team.join("TEAM.md"), "---\nname: addteam\nobjective: Add probe.\nprovider: codex\n---\n\nteam.\n").unwrap();
673
+ std::fs::write(team.join("agents").join("implementer.md"), QS_VALID_ROLE).unwrap(); // existing agent
674
+ let role_file = team.join("worker2-role.md"); // OUTSIDE agents/ -> not a duplicate of an existing agent
675
+ std::fs::write(&role_file, DELEG_ROLE_WORKER2).unwrap();
676
+ seed_healthy_coordinator(&team);
677
+ let transport = OfflineTransport::new();
678
+
679
+ let _result = add_agent_with_transport(&team, &AgentId::new("worker2"), &role_file, false, None, &transport);
680
+
681
+ // (a) the recompiled spec was written (real subsystem step — works today).
682
+ assert!(team.join("team.spec.yaml").exists(), "add_agent must recompile + write team.spec.yaml under the team dir");
683
+ // (b) the new worker window was spawned (RED: stub recompiles then RequirementUnmet -> ZERO spawns).
684
+ let recorded = transport.spawn_records();
685
+ assert!(
686
+ !recorded.is_empty(),
687
+ "add_agent must spawn the new worker window (>=1 recorded spawn); the rt-host-a stub recompiles \
688
+ then returns RequirementUnmet with ZERO spawns; got {recorded:?}"
689
+ );
690
+ assert!(
691
+ recorded.iter().any(|(_, argv)| argv.iter().any(|a| a == "codex")),
692
+ "the new worker's spawn argv must carry the provider build_command (codex); got {recorded:?}"
693
+ );
694
+ }
695
+
696
+ // ═════════════════════════════════════════════════════════════════════════
697
+ // WAVE 2 · LANE A — agent-lifecycle byte-parity contracts. stop_agent / reset_agent / remove_agent /
698
+ // fork_agent are stubs (OwnerRefused / RequirementUnmet / "session_id missing"). These RED contracts
699
+ // LOCK the golden behavior (lifecycle/operations.py + agents.py). The existing tolerant tests accept the
700
+ // stub; these are STRICTER (RED today). OS-safe: no-owner ws + seeded spec/state, non-running agents
701
+ // (pure fs/state). Transport-asserting parts (kill_window / native-fork spawn) -> seam + #[ignore].
702
+ // ═════════════════════════════════════════════════════════════════════════
703
+
704
+ // ═══════════════════════════════════════════════════════════════════════════
705
+ // collect #223 — task-scoped seeding (RED). Golden launch/core.py:69 ALWAYS seeds the runtime
706
+ // state's `tasks` key from spec.tasks (≥ []); the doc compiler emits a default task
707
+ // {id:"task_initial",…} (compiler.rs:308). Rust initial_runtime_state (launch.rs) emits only
708
+ // {session_name,team_dir,agents} — NO tasks key → send/collect cannot resolve the task. RED.
709
+ // OS-safe: OfflineTransport (zero real spawn).
710
+ // ═══════════════════════════════════════════════════════════════════════════
711
+ #[test]
712
+ fn quick_start_seeds_tasks_key_from_compiled_spec() {
713
+ let team = quick_start_team_dir(QS_VALID_ROLE);
714
+ let transport = OfflineTransport::new();
715
+ let _ = quick_start_with_transport(&team, None, true, true, None, &transport);
716
+ let workspace = team.parent().expect("team_workspace(<base>/teamdir) = <base>");
717
+ let state = crate::state::persist::load_runtime_state(workspace)
718
+ .expect("runtime state.json must exist after quick_start");
719
+ // (b) the tasks KEY must always be present and a JSON array (golden launch/core.py:69 default []).
720
+ let tasks = match state.get("tasks") {
721
+ Some(t) => t,
722
+ None => panic!(
723
+ "initial_runtime_state MUST seed a `tasks` key (golden launch/core.py:69 `tasks=[…spec.tasks]`); \
724
+ state keys = {:?}",
725
+ state.as_object().map(|o| o.keys().cloned().collect::<Vec<_>>())
726
+ ),
727
+ };
728
+ let tasks = tasks.as_array().expect("`tasks` must be a JSON array (golden default [])");
729
+ // (a) spec.tasks must be carried into runtime state — the doc compiler's default task id.
730
+ assert!(
731
+ tasks.iter().any(|t| t.get("id").and_then(|v| v.as_str()) == Some("task_initial")),
732
+ "state.tasks must carry the compiled spec's task (id=task_initial); got {tasks:?}"
733
+ );
734
+ }
735
+
736
+ // Stage A — golden launch/core.py:62-71 seeded top-level runtime state in insertion order:
737
+ // spec_path, workspace, team_dir, session_name, leader, agents, tasks, display_backend.
738
+ //
739
+ // OLD (Python parity): the top-level shape ended at `display_backend`.
740
+ // NEW (Bug 1/2 — team-in-team state scope, see tests/team_in_team_state_scope_red.rs):
741
+ // `active_team_key` and `teams` are appended at the tail so the runtime can carry the
742
+ // nested team-in-team scope alongside the original flat fields. Owner-binding fields
743
+ // (`leader_receiver` / `team_owner` / `owner_epoch`) live ONLY under
744
+ // `teams[<active_team_key>]` — Bug 2 owner team-scope (N1/N12/N18/N29) deliberately
745
+ // keeps them OFF the root so per-team isolation has a single source of truth and
746
+ // cross-team reads cannot accidentally pick up another team's binding from the root.
747
+ // The post-launch hook drops the top-level owner triple when the launched pane is
748
+ // unbound (empty pane_id), and only the per-team entry retains the binding.
749
+ // Order remains the golden prefix (spec_path … display_backend) + new suffix
750
+ // (active_team_key, teams). display_backend stays the resolved backend from
751
+ // display/backend.py:12-29 (default adaptive), not the raw optional spec field.
752
+ #[test]
753
+ #[serial(env)]
754
+ fn quick_start_state_seeds_spec_path_workspace_leader_display_backend() {
755
+ // Bug 2 owner team-scope: top-level owner triple is dropped when the seeded
756
+ // pane is empty; an ambient TMUX_PANE (tests run inside the dev's tmux session)
757
+ // would otherwise leak into the caller-identity seed, inflate the seeded owner
758
+ // with a non-empty pane, and keep the top-level keys present — masking the
759
+ // per-team-isolation invariant this assertion locks. Force-unset for the test.
760
+ let _tmux_pane = EnvVarGuard::unset("TMUX_PANE");
761
+ let _tmux = EnvVarGuard::unset("TMUX");
762
+ let team = quick_start_team_dir(QS_VALID_ROLE);
763
+ let transport = OfflineTransport::new();
764
+ let _ = quick_start_with_transport(&team, None, true, true, None, &transport);
765
+ let workspace = team.parent().expect("team_workspace(<base>/teamdir) = <base>");
766
+ let spec_path = team.join("team.spec.yaml");
767
+ let (raw, state) = raw_runtime_state(workspace);
768
+ let keys = state.as_object().expect("state root object").keys().cloned().collect::<Vec<_>>();
769
+ assert_eq!(
770
+ keys,
771
+ vec![
772
+ "spec_path",
773
+ "workspace",
774
+ "team_dir",
775
+ "session_name",
776
+ "leader",
777
+ "agents",
778
+ "tasks",
779
+ "display_backend",
780
+ "active_team_key",
781
+ "teams",
782
+ ],
783
+ "state.json top-level key order must match golden launch/core.py:62-71 \
784
+ plus Bug 1/2 team-in-team suffix (active_team_key, teams). \
785
+ Bug 2 owner team-scope (N1/N12/N18/N29) deliberately keeps owner / \
786
+ leader_receiver / owner_epoch OFF the top level — they live ONLY under \
787
+ teams[<active_team_key>] so per-team isolation has a single source of \
788
+ truth (no shadow copy on the root that callers could read across teams); \
789
+ raw={raw}"
790
+ );
791
+ assert_eq!(state["spec_path"], json!(spec_path.canonicalize().unwrap().to_string_lossy()));
792
+ assert_eq!(state["workspace"], json!(workspace.to_string_lossy()));
793
+ assert_eq!(state["team_dir"], json!(team.to_string_lossy()));
794
+ assert_eq!(state["session_name"], json!("team-quickteam"));
795
+ assert_eq!(
796
+ state["leader"]["id"],
797
+ json!("leader"),
798
+ "leader must be copied from compiled spec.leader"
799
+ );
800
+ assert!(
801
+ state["agents"].as_object().is_some_and(|agents| agents.contains_key("implementer")),
802
+ "existing agents value must remain seeded from compiled spec; got {:?}",
803
+ state["agents"]
804
+ );
805
+ assert!(
806
+ state["tasks"].as_array().is_some_and(|tasks| {
807
+ tasks.iter().any(|task| task.get("id").and_then(|id| id.as_str()) == Some("task_initial"))
808
+ }),
809
+ "existing tasks value must remain seeded from compiled spec; got {:?}",
810
+ state["tasks"]
811
+ );
812
+ assert_eq!(
813
+ state["display_backend"],
814
+ json!("adaptive"),
815
+ "golden resolve_display_backend(None, source='launch') defaults to adaptive"
816
+ );
817
+ }
818
+
819
+ // Stage B1 — golden launch/core.py:238-255 writes the running agent state only after
820
+ // a successful spawn. The fixed clock is a test seam for `spawned_at`; capture intentionally
821
+ // misses, so session/capture fields remain present JSON nulls. MCP install is deterministic:
822
+ // provider_cli/adapter.py:111-114 writes workspace/.team/runtime/mcp/<agent>.json.
823
+ #[test]
824
+ #[serial(env)]
825
+ fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
826
+ const FIXED_SPAWNED_AT: &str = "2026-06-04T00:00:00+00:00";
827
+ let _clock_guard =
828
+ EnvVarGuard::set("TEAM_AGENT_TEST_FIXED_SPAWNED_AT", FIXED_SPAWNED_AT);
829
+ let team = quick_start_team_dir(QS_VALID_ROLE);
830
+ let transport = OfflineTransport::new();
831
+ let _ = quick_start_with_transport(&team, None, true, true, None, &transport);
832
+ let workspace = team.parent().expect("team_workspace(<base>/teamdir) = <base>");
833
+ let (raw, state) = raw_runtime_state(workspace);
834
+ let agent = state
835
+ .pointer("/agents/implementer")
836
+ .unwrap_or_else(|| panic!("implementer agent state missing; raw={raw}"));
837
+ let keys = agent.as_object().expect("agent state object").keys().cloned().collect::<Vec<_>>();
838
+ assert_eq!(
839
+ keys,
840
+ vec![
841
+ "status",
842
+ "provider",
843
+ "agent_id",
844
+ "model",
845
+ "auth_mode",
846
+ "profile",
847
+ "window",
848
+ "mcp_config",
849
+ "permissions",
850
+ "session_id",
851
+ "rollout_path",
852
+ "captured_at",
853
+ "captured_via",
854
+ "attribution_confidence",
855
+ "spawn_cwd",
856
+ "spawned_at",
857
+ ],
858
+ "running agent state key order must match golden launch/core.py:238-255; raw={raw}"
859
+ );
860
+ assert_eq!(agent["status"], json!("running"));
861
+ assert_eq!(agent["provider"], json!("codex"));
862
+ assert_eq!(agent["agent_id"], json!("implementer"));
863
+ assert_eq!(agent["model"], json!("gpt-5.5"));
864
+ assert_eq!(agent["auth_mode"], json!("subscription"));
865
+ assert!(agent["profile"].is_null());
866
+ assert_eq!(agent["window"], json!("implementer"));
867
+ assert_eq!(
868
+ agent["mcp_config"],
869
+ json!(workspace.join(".team/runtime/mcp/implementer.json").to_string_lossy())
870
+ );
871
+ assert_eq!(
872
+ agent["permissions"],
873
+ json!({
874
+ "agent_id": "implementer",
875
+ "provider": "codex",
876
+ "tools": ["mcp_team"],
877
+ "resolved_tools": [{"tool": "mcp_team", "enforcement": "prompt_only"}],
878
+ "has_prompt_only": true
879
+ })
880
+ );
881
+ assert!(agent["session_id"].is_null());
882
+ assert!(agent["rollout_path"].is_null());
883
+ assert!(agent["captured_at"].is_null());
884
+ assert!(agent["captured_via"].is_null());
885
+ assert!(agent["attribution_confidence"].is_null());
886
+ assert_eq!(agent["spawn_cwd"], json!(workspace.to_string_lossy()));
887
+ assert_eq!(agent["spawned_at"], json!(FIXED_SPAWNED_AT));
888
+ }
889
+
890
+ // Stage B2 — golden launch/core.py:171-173 writes paused workers as exactly
891
+ // {status, provider} and skips spawn entirely.
892
+ #[test]
893
+ fn quick_start_paused_agent_state_is_paused_provider_only_and_not_spawned() {
894
+ let team = quick_start_team_dir(QS_VALID_ROLE);
895
+ let spec_path = compiled_spec_path_with_paused_agent(&team);
896
+ let workspace = team.parent().expect("team_workspace(<base>/teamdir) = <base>");
897
+ crate::state::persist::save_runtime_state(
898
+ workspace,
899
+ &json!({
900
+ "session_name": "team-quickteam",
901
+ "team_dir": team.to_string_lossy(),
902
+ "agents": {
903
+ "implementer": {
904
+ "provider": "codex",
905
+ "role": "Implementation Engineer",
906
+ "model": "gpt-5.5",
907
+ "auth_mode": "subscription"
908
+ }
909
+ },
910
+ "tasks": []
911
+ }),
912
+ )
913
+ .expect("seed pre-launch runtime state");
914
+ let transport = OfflineTransport::new();
915
+ let _ = launch_with_transport(&spec_path, false, true, true, &transport);
916
+ let (raw, state) = raw_runtime_state(workspace);
917
+ let agent = state
918
+ .pointer("/agents/implementer")
919
+ .unwrap_or_else(|| panic!("implementer agent state missing; raw={raw}"));
920
+ let keys = agent.as_object().expect("agent state object").keys().cloned().collect::<Vec<_>>();
921
+ assert_eq!(
922
+ keys,
923
+ vec!["status", "provider"],
924
+ "paused agent state must be exactly golden launch/core.py:171-173; raw={raw}"
925
+ );
926
+ assert_eq!(agent["status"], json!("paused"));
927
+ assert_eq!(agent["provider"], json!("codex"));
928
+ assert!(
929
+ transport.spawn_records().is_empty(),
930
+ "paused agent must not spawn any terminal window; got {:?}",
931
+ transport.spawn_records()
932
+ );
933
+ }