@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,675 @@
1
+ use super::*;
2
+
3
+ fn cli_argv(items: &[&str]) -> Vec<String> {
4
+ items.iter().map(|s| (*s).to_string()).collect()
5
+ }
6
+
7
+ const FAKE_SPEC_YAML: &str = r#"version: 1
8
+ team:
9
+ name: "fake-e2e"
10
+ mode: "supervisor_worker"
11
+ objective: "Exercise fake provider orchestration."
12
+ workspace: "/WS"
13
+ leader:
14
+ id: "leader"
15
+ role: "leader"
16
+ provider: "fake"
17
+ model: null
18
+ tools:
19
+ - "fs_read"
20
+ - "fs_list"
21
+ - "mcp_team"
22
+ context_policy:
23
+ keep_user_thread: true
24
+ receive_worker_outputs: "structured_only"
25
+ max_worker_result_tokens: 2000
26
+ agents:
27
+ - id: "fake_impl"
28
+ role: "implementation_engineer"
29
+ provider: "fake"
30
+ model: null
31
+ working_directory: "/WS"
32
+ system_prompt:
33
+ inline: "Handle fake implementation tasks."
34
+ file: null
35
+ tools:
36
+ - "fs_read"
37
+ - "fs_write"
38
+ - "fs_list"
39
+ - "execute_bash"
40
+ - "git_diff"
41
+ - "mcp_team"
42
+ - "provider_builtin"
43
+ permission_mode: "restricted"
44
+ preferred_for:
45
+ - "implementation"
46
+ avoid_for: []
47
+ output_contract:
48
+ format: "result_envelope_v1"
49
+ required_fields:
50
+ - "task_id"
51
+ - "status"
52
+ - "summary"
53
+ - "artifacts"
54
+ routing:
55
+ default_assignee: "leader"
56
+ rules:
57
+ - id: "implementation-to-fake"
58
+ match:
59
+ type:
60
+ - "implementation"
61
+ assign_to: "fake_impl"
62
+ priority: 10
63
+ communication:
64
+ protocol: "mcp_inbox"
65
+ topology: "leader_centered"
66
+ worker_to_worker: true
67
+ ack_timeout_sec: 2
68
+ result_format: "result_envelope_v1"
69
+ message_store:
70
+ sqlite: ".team/runtime/team.db"
71
+ mirror_files: ".team/messages"
72
+ runtime:
73
+ backend: "tmux"
74
+ display_backend: "none"
75
+ session_name: "team-agent-fake-e2e"
76
+ auto_launch: true
77
+ require_user_approval_before_launch: false
78
+ max_active_agents: 1
79
+ startup_order:
80
+ - "fake_impl"
81
+ context:
82
+ state_file: "team_state.md"
83
+ artifact_dir: ".team/artifacts"
84
+ log_dir: ".team/logs"
85
+ summarization:
86
+ worker_full_logs: "retain_outside_leader_context"
87
+ state_update: "after_each_result"
88
+ tasks:
89
+ - id: "task_impl"
90
+ title: "Fake implementation"
91
+ type: "implementation"
92
+ assignee: null
93
+ deps: []
94
+ acceptance:
95
+ - "fake result collected"
96
+ status: "pending"
97
+ requires_tools:
98
+ - "fs_write"
99
+ - "execute_bash"
100
+ files:
101
+ - "src/example.py"
102
+ risk: "low"
103
+ "#;
104
+
105
+ fn seed_team_spec(ws: &std::path::Path) {
106
+ let spec = FAKE_SPEC_YAML.replace("/WS", &ws.to_string_lossy());
107
+ std::fs::write(ws.join("team.spec.yaml"), spec).unwrap();
108
+ }
109
+
110
+ // ── ACK-CRACK [P1 byte-shape] — acknowledge_idle must write golden's TTL suppression shape ───────
111
+ // golden runtime.py:680-688: manual-acknowledge persists
112
+ // coordinator.idle_acknowledged[team] = {acknowledged_at, expires_at, ttl_seconds}
113
+ // coordinator.suppressed_idle_alerts[team][worker].idle_fallback =
114
+ // {suppressed_at, suppressed_by:"manual_acknowledge", manual_acknowledge:true, expires_at, ttl_seconds}
115
+ // The clear logic does datetime.fromisoformat(entry["expires_at"]); a MISSING expires_at -> ValueError
116
+ // -> "invalid_suppression_timestamp" -> immediate self-clear (latent crack once detect_idle_fallbacks
117
+ // is ported). So BOTH idle_acknowledged and the entry MUST carry a non-empty expires_at.
118
+ #[test]
119
+ fn acknowledge_idle_writes_golden_ttl_suppression_shape() {
120
+ let ws = tmp_workspace();
121
+ crate::state::persist::save_runtime_state(
122
+ &ws,
123
+ &serde_json::json!({
124
+ "active_team_key": "teamX",
125
+ "agents": {"w1": {"status": "running", "provider": "codex"}}
126
+ }),
127
+ )
128
+ .unwrap();
129
+ let _ = lifecycle_port::acknowledge_idle(&ws, None).expect("acknowledge_idle ok");
130
+ let state = crate::state::persist::load_runtime_state(&ws).unwrap();
131
+ let ack = &state["coordinator"]["idle_acknowledged"]["teamX"];
132
+ assert!(
133
+ ack.get("expires_at").and_then(serde_json::Value::as_str).is_some_and(|s| !s.is_empty()),
134
+ "ACK-CRACK: idle_acknowledged[team] must carry a non-empty expires_at (golden); got {ack}"
135
+ );
136
+ assert!(ack.get("ttl_seconds").is_some(), "idle_acknowledged[team] must carry ttl_seconds; got {ack}");
137
+ let entry = &state["coordinator"]["suppressed_idle_alerts"]["teamX"]["w1"]["idle_fallback"];
138
+ assert!(
139
+ entry.get("expires_at").and_then(serde_json::Value::as_str).is_some_and(|s| !s.is_empty()),
140
+ "ACK-CRACK: the manual-ack suppression entry must carry expires_at (else clear logic ValueErrors \
141
+ -> instant self-clear); got {entry}"
142
+ );
143
+ assert_eq!(entry["suppressed_by"], serde_json::json!("manual_acknowledge"), "golden suppressed_by; got {entry}");
144
+ assert_eq!(entry["manual_acknowledge"], serde_json::json!(true), "golden manual_acknowledge:true; got {entry}");
145
+ }
146
+ // ── ACK return-shape [P1 byte-parity] — acknowledge_idle must RETURN golden's keys ───────────────
147
+ // golden runtime.py:691: return {ok, team, agent_id, acknowledged_at, expires_at, ttl_seconds}.
148
+ // Rust (cli/mod.rs) returns only {ok, team, ttl_seconds} -> missing agent_id, acknowledged_at,
149
+ // expires_at. RED. (acknowledged_at/expires_at are the same values written into idle_acknowledged.)
150
+ #[test]
151
+ fn acknowledge_idle_return_carries_golden_keys() {
152
+ let ws = tmp_workspace();
153
+ crate::state::persist::save_runtime_state(
154
+ &ws,
155
+ &serde_json::json!({ "active_team_key": "teamX", "agents": {"w1": {"status": "running", "provider": "codex"}} }),
156
+ )
157
+ .unwrap();
158
+ let r = lifecycle_port::acknowledge_idle(&ws, None).expect("acknowledge_idle ok");
159
+ let obj = r.as_object().expect("ack returns a dict");
160
+ for key in ["ok", "team", "agent_id", "acknowledged_at", "expires_at", "ttl_seconds"] {
161
+ assert!(
162
+ obj.contains_key(key),
163
+ "ACK return-shape: golden return carries `{key}` (runtime.py:691: ok/team/agent_id/\
164
+ acknowledged_at/expires_at/ttl_seconds); Rust omits it. got keys {:?}",
165
+ obj.keys().collect::<Vec<_>>()
166
+ );
167
+ }
168
+ assert!(
169
+ obj.get("expires_at").and_then(serde_json::Value::as_str).is_some_and(|s| !s.is_empty()),
170
+ "ACK return-shape: expires_at must be a non-empty timestamp; got {r}"
171
+ );
172
+ let _ = std::fs::remove_dir_all(&ws);
173
+ }
174
+ // ── BUG-2 [real bug] — inbox must RETURN the stored messages, not a hardcoded []. ────────────────
175
+ // Golden status/inbox.py:35-38 -> MessageStore.inbox(agent_id) (core.py:242, owner_team_id=None):
176
+ // select <MESSAGE_SELECT> from messages where sender = ? or recipient = ? order by created_at desc
177
+ // limit ? -> then reversed(rows) (chronological asc). At THIS call site owner_team_id is None, so
178
+ // there is NO team filter — a sent-and-stored message (recipient=w1, status='accepted') must show
179
+ // in `inbox w1`. Rust mod.rs:144 is a stub: `let _=(workspace,limit,as_json); "messages":[]`. So
180
+ // the row is in team.db but inbox always returns [] -> RED. The shape test above only proves the
181
+ // empty-state envelope; THIS proves the message actually surfaces.
182
+ #[test]
183
+ fn inbox_returns_stored_message_for_recipient() {
184
+ let ws = tmp_workspace();
185
+ let store = crate::message_store::MessageStore::open(&ws).unwrap();
186
+ let mid = store
187
+ .create_message(None, "leader", "w1", "hello w1", None, true, None)
188
+ .unwrap();
189
+ let v = status_port::inbox(&ws, "w1", 20, None, true).expect("inbox");
190
+ let messages = v["messages"].as_array().expect("messages array");
191
+ assert_eq!(
192
+ messages.len(),
193
+ 1,
194
+ "golden inbox(w1) must return the stored recipient=w1 row; the stub returns [] -> RED. got {v}"
195
+ );
196
+ let m = &messages[0];
197
+ assert_eq!(m["message_id"], json!(mid), "the returned row is the message we stored");
198
+ assert_eq!(m["recipient"], json!("w1"));
199
+ assert_eq!(m["sender"], json!("leader"));
200
+ assert_eq!(m["content"], json!("hello w1"));
201
+ assert_eq!(m["status"], json!("accepted"), "create_message persists status='accepted'");
202
+ // NULL owner_team_id semantics: status.inbox() calls MessageStore.inbox(agent) with
203
+ // owner_team_id=None (no team clause), so a NULL-owner message MUST surface for its recipient.
204
+ assert_eq!(m["owner_team_id"], json!(null), "the stored message's owner_team_id is NULL and still returned");
205
+ // byte-faithful raw-row columns: requires_ack is the 0/1 INT; artifact_refs the literal text "[]".
206
+ assert_eq!(m["requires_ack"], json!(1), "requires_ack is the 0/1 int, not a bool");
207
+ assert_eq!(m["artifact_refs"], json!("[]"), "artifact_refs is the raw text column, not parsed");
208
+ let _ = std::fs::remove_dir_all(&ws);
209
+ }
210
+ // ── BUG-2 (match scope) — inbox(agent) returns rows where sender==agent OR recipient==agent, and
211
+ // EXCLUDES messages for other agents. Membership+exclusion form (not strict index order) so the
212
+ // test is deterministic regardless of created_at sub-second ties; golden order is chronological asc. ─
213
+ #[test]
214
+ fn inbox_matches_sender_or_recipient_and_excludes_others() {
215
+ let ws = tmp_workspace();
216
+ let store = crate::message_store::MessageStore::open(&ws).unwrap();
217
+ store.create_message(None, "leader", "w1", "to w1", None, true, None).unwrap();
218
+ store.create_message(None, "w1", "leader", "from w1", None, true, None).unwrap();
219
+ store.create_message(None, "leader", "w2", "unrelated to w2", None, true, None).unwrap();
220
+ let v = status_port::inbox(&ws, "w1", 20, None, true).expect("inbox");
221
+ let messages = v["messages"].as_array().expect("messages array");
222
+ let mut contents: Vec<String> =
223
+ messages.iter().map(|m| m["content"].as_str().unwrap().to_string()).collect();
224
+ contents.sort();
225
+ assert_eq!(
226
+ contents,
227
+ vec!["from w1".to_string(), "to w1".to_string()],
228
+ "inbox(w1) must return BOTH the recipient=w1 and sender=w1 rows and EXCLUDE the w2 message; \
229
+ the stub returns [] -> RED. got {contents:?}"
230
+ );
231
+ let _ = std::fs::remove_dir_all(&ws);
232
+ }
233
+ // ── BUG-4 [real bug] — peek must resolve the agent terminal via state `session:window` (golden),
234
+ // NOT a stored `pane_id` field. A live worker present in state (with a `window`, on the team's `-L`
235
+ // socket) must NOT be fabricated as "agent pane not found". ──────────────────────────────────────
236
+ // Golden status/peek.py:35-44: agent = state["agents"][id]; window = agent.get("window", id);
237
+ // if not session_name or not _tmux_window_exists(session_name, window): raise "agent terminal is
238
+ // not available: <id>"; else `tmux capture-pane -t session:window`. It NEVER reads a stored pane_id.
239
+ // Probed live (/tmp/probe_peek.py): a present worker whose window is absent on the socket raises
240
+ // `agent terminal is not available: w1`; a missing agent raises `unknown agent id: <id>`. Rust
241
+ // cmd_peek (adapters.rs:231 + agent_pane_id:279) keys off agent_state pane_id/pane/tmux_pane_id and
242
+ // returns {ok:false,error:"agent pane not found"} when absent — so a NORMAL live worker (window in
243
+ // state, no pane_id field) is mis-reported as not found. That is the CP-1 pane-resolution divergence.
244
+ //
245
+ // Deterministic without real tmux: on a host with no live session, the golden-correct peek resolves
246
+ // session:window, finds the window absent on the socket, and yields "agent terminal is not available:
247
+ // w1" — NOT "agent pane not found". (The window-on-socket -> real raw-screen capture positive case is
248
+ // real-machine; see the #[ignore] dispatch_routes_peek_real_machine.)
249
+ #[test]
250
+ fn peek_resolves_live_worker_via_session_window_not_pane_id_field() {
251
+ let ws = tmp_workspace();
252
+ // a live worker: present in state with a `window`, session_name set, but NO stored pane_id field.
253
+ // session name is unique so a real tmux session on the dev host can't accidentally satisfy it.
254
+ crate::state::persist::save_runtime_state(
255
+ &ws,
256
+ &json!({
257
+ "session_name": "team-peek-red-probe-x9q",
258
+ "agents": {"w1": {"status": "running", "provider": "codex", "window": "w1"}}
259
+ }),
260
+ )
261
+ .unwrap();
262
+ let args = PeekArgs {
263
+ agent: "w1".to_string(),
264
+ workspace: ws.clone(),
265
+ tail: 20,
266
+ allow_raw_screen: true,
267
+ json: true,
268
+ };
269
+ let text = outcome_text(cmd_peek(&args));
270
+ assert!(
271
+ !text.contains("agent pane not found"),
272
+ "peek keys off a stored pane_id field and fabricates 'agent pane not found' for a live worker \
273
+ that has a `window` in state; golden resolves session:window and never reads pane_id. got: {text}"
274
+ );
275
+ assert!(
276
+ text.contains("agent terminal is not available: w1"),
277
+ "golden status/peek.py: a worker whose window is not on the socket yields \
278
+ 'agent terminal is not available: w1' (window-existence via session:window), NOT a \
279
+ pane_id-keyed error. got: {text}"
280
+ );
281
+ let _ = std::fs::remove_dir_all(&ws);
282
+ }
283
+ #[test]
284
+ fn ux_doctor_secret_scan_is_present_and_non_triggering_for_normal_paths() {
285
+ let ws = tmp_workspace();
286
+ std::fs::write(ws.join("normal-role.md"), "---\nname: worker\nprovider: codex\n---\nUse /tmp/team-agent.\n").unwrap();
287
+ let value = json_output(cmd_doctor(&DoctorArgs {
288
+ spec: None,
289
+ workspace: ws.clone(),
290
+ gate: None,
291
+ comms: false,
292
+ team: None,
293
+ fix: false,
294
+ fix_schema: false,
295
+ cleanup_orphans: false,
296
+ confirm: false,
297
+ json: true,
298
+ }).expect("doctor"));
299
+ assert_eq!(value.pointer("/secret_scan/ok"), Some(&json!(true)));
300
+ assert_eq!(value.pointer("/secret_scan/findings"), Some(&json!([])));
301
+ let _ = std::fs::remove_dir_all(&ws);
302
+ }
303
+ #[test]
304
+ fn ux_doctor_secret_scan_findings_name_the_exact_trigger() {
305
+ let ws = tmp_workspace();
306
+ std::fs::write(ws.join("leaky-role.md"), "OPENAI_API_KEY=sk-test-red-contract\n").unwrap();
307
+ let value = json_output(cmd_doctor(&DoctorArgs {
308
+ spec: None,
309
+ workspace: ws.clone(),
310
+ gate: None,
311
+ comms: false,
312
+ team: None,
313
+ fix: false,
314
+ fix_schema: false,
315
+ cleanup_orphans: false,
316
+ confirm: false,
317
+ json: true,
318
+ }).expect("doctor"));
319
+ let finding = value
320
+ .pointer("/secret_scan/findings/0")
321
+ .and_then(serde_json::Value::as_object)
322
+ .expect("secret-scan must report the concrete trigger");
323
+ for key in ["path", "line", "rule", "match_excerpt"] {
324
+ assert!(finding.contains_key(key), "secret-scan finding missing `{key}`: {finding:?}");
325
+ }
326
+ let _ = std::fs::remove_dir_all(&ws);
327
+ }
328
+ #[test]
329
+ fn ux_wait_ready_does_not_report_ready_true_without_ready_runtime_state() {
330
+ let ws = tmp_workspace();
331
+ crate::state::persist::save_runtime_state(
332
+ &ws,
333
+ &json!({
334
+ "agents": {"w1": {"status": "starting"}},
335
+ "tasks": [{"id": "t1", "assignee": "w1", "status": "pending"}],
336
+ "leader_receiver": {"status": "attached"},
337
+ }),
338
+ )
339
+ .unwrap();
340
+ let value = json_output(cmd_wait_ready(&WaitReadyArgs {
341
+ workspace: ws.clone(),
342
+ timeout: 0.0,
343
+ json: true,
344
+ }).expect("wait-ready"));
345
+ assert_eq!(value["ok"], json!(false), "wait-ready must not fake success before workers are ready");
346
+ assert_eq!(value.pointer("/readiness/ready"), Some(&json!(false)));
347
+ assert!(
348
+ value["summary"].as_str().unwrap_or("").contains("not ready"),
349
+ "wait-ready false state should explain not-ready status, got {value:?}"
350
+ );
351
+ let _ = std::fs::remove_dir_all(&ws);
352
+ }
353
+ #[test]
354
+ fn wait_ready_fake_quick_start_counts_mcp_config_and_task_prompt_delivery() {
355
+ let ws = tmp_workspace();
356
+ let mcp_config = ws.join(".team").join("runtime").join("agents").join("fake_impl").join("mcp_config.json");
357
+ std::fs::create_dir_all(mcp_config.parent().unwrap()).unwrap();
358
+ std::fs::write(&mcp_config, r#"{"mcpServers":{"team-agent":{}}}"#).unwrap();
359
+ crate::state::persist::save_runtime_state(
360
+ &ws,
361
+ &json!({
362
+ "session_name": "team-fake-ready",
363
+ "agents": {
364
+ "fake_impl": {
365
+ "status": "running",
366
+ "provider": "fake",
367
+ "mcp_config": mcp_config.to_string_lossy(),
368
+ }
369
+ },
370
+ "tasks": [{
371
+ "id": "task_impl",
372
+ "assignee": "fake_impl",
373
+ "status": "pending",
374
+ }],
375
+ "leader_receiver": {"status": "attached"},
376
+ }),
377
+ )
378
+ .unwrap();
379
+ let store = crate::message_store::MessageStore::open(&ws).unwrap();
380
+ store
381
+ .create_message(Some("task_impl"), "leader", "fake_impl", "initial task prompt", None, true, None)
382
+ .unwrap();
383
+ let value = json_output(cmd_wait_ready(&WaitReadyArgs {
384
+ workspace: ws.clone(),
385
+ timeout: 0.0,
386
+ json: true,
387
+ }).expect("wait-ready"));
388
+ assert_eq!(
389
+ value.pointer("/readiness/mcp_ready"),
390
+ Some(&json!(true)),
391
+ "fake quick-start readiness must treat an existing per-agent mcp_config file as mcp_ready"
392
+ );
393
+ assert_eq!(
394
+ value.pointer("/readiness/task_prompt_delivered"),
395
+ Some(&json!(true)),
396
+ "fake quick-start readiness must treat message_counts>0 / persisted initial task prompt as task_prompt_delivered"
397
+ );
398
+ assert_eq!(
399
+ value.pointer("/readiness/ready"),
400
+ Some(&json!(true)),
401
+ "process_started + cli_prompt_ready alone is incomplete; mcp_ready and task_prompt_delivered must also be satisfied"
402
+ );
403
+ let _ = std::fs::remove_dir_all(&ws);
404
+ }
405
+ fn valid_result_envelope() -> serde_json::Value {
406
+ json!({
407
+ "schema_version": "result_envelope_v1",
408
+ "task_id": "task_impl",
409
+ "agent_id": "fake_impl",
410
+ "status": "success",
411
+ "summary": "done",
412
+ "artifacts": [],
413
+ "changes": [],
414
+ "tests": [{"command": "cargo test", "status": "passed"}],
415
+ "risks": [],
416
+ "next_actions": []
417
+ })
418
+ }
419
+ fn seed_collect_state(ws: &std::path::Path) {
420
+ seed_team_spec(ws);
421
+ crate::state::persist::save_runtime_state(
422
+ ws,
423
+ &json!({
424
+ "agents": {"fake_impl": {"status": "idle"}},
425
+ "tasks": [{
426
+ "id": "task_impl",
427
+ "title": "Fake implementation",
428
+ "type": "implementation",
429
+ "assignee": "fake_impl",
430
+ "deps": [],
431
+ "acceptance": ["fake result collected"],
432
+ "status": "pending",
433
+ "requires_tools": [],
434
+ "files": [],
435
+ "risk": "low"
436
+ }],
437
+ "session_name": Value::Null,
438
+ "active_team_key": Value::Null,
439
+ "spec_path": ws.join("team.spec.yaml").to_string_lossy()
440
+ }),
441
+ )
442
+ .unwrap();
443
+ }
444
+ fn seed_uncollected_result(ws: &std::path::Path, result_id: &str) {
445
+ let store = crate::message_store::MessageStore::open(ws).unwrap();
446
+ let conn = crate::db::schema::open_db(store.db_path()).unwrap();
447
+ conn.execute(
448
+ "insert into results(
449
+ result_id, owner_team_id, task_id, agent_id, envelope, status, created_at
450
+ ) values (?1, null, 'task_impl', 'fake_impl', ?2, 'success', '2026-06-02T10:00:00+00:00')",
451
+ rusqlite::params![result_id, valid_result_envelope().to_string()],
452
+ )
453
+ .unwrap();
454
+ }
455
+ fn read_state(ws: &std::path::Path) -> serde_json::Value {
456
+ serde_json::from_str(
457
+ &std::fs::read_to_string(crate::state::persist::runtime_state_path(ws)).unwrap(),
458
+ )
459
+ .unwrap()
460
+ }
461
+ fn read_events(ws: &std::path::Path) -> Vec<serde_json::Value> {
462
+ crate::event_log::EventLog::new(ws).tail(50).unwrap()
463
+ }
464
+ fn seeded_team_key(ws: &std::path::Path) -> String {
465
+ ws.file_name().unwrap().to_string_lossy().to_string()
466
+ }
467
+ fn json_output(result: CmdResult) -> serde_json::Value {
468
+ match result.output {
469
+ CmdOutput::Json(v) => v,
470
+ other => panic!("expected JSON output, got {other:?}"),
471
+ }
472
+ }
473
+ #[test]
474
+ fn validate_result_file_good_and_inline_garbage_are_distinct() {
475
+ let ws = tmp_workspace();
476
+ let envelope_path = ws.join("result.json");
477
+ std::fs::write(&envelope_path, valid_result_envelope().to_string()).unwrap();
478
+ let good = run(
479
+ &cli_argv(&["validate-result", "--file", &envelope_path.to_string_lossy(), "--json"]),
480
+ &ws,
481
+ );
482
+ assert_eq!(
483
+ good,
484
+ ExitCode::Ok,
485
+ "Python cmd_validate_result accepts --file and returns {{ok:true,task_id,agent_id,status}} for a valid envelope"
486
+ );
487
+ let garbage = run(&cli_argv(&["validate-result", "{garbage", "--json"]), &ws);
488
+ assert_eq!(
489
+ garbage,
490
+ ExitCode::Error,
491
+ "garbage JSON must be invalid, not indistinguishable from the good-envelope path"
492
+ );
493
+ }
494
+ #[test]
495
+ fn collect_uncollected_result_marks_db_and_outputs_result() {
496
+ let ws = tmp_workspace();
497
+ seed_collect_state(&ws);
498
+ seed_uncollected_result(&ws, "res_collect_red");
499
+ let out = json_output(
500
+ cmd_collect(&CollectArgs {
501
+ workspace: ws.clone(),
502
+ result_file: None,
503
+ json: true,
504
+ })
505
+ .unwrap(),
506
+ );
507
+ assert_eq!(out["ok"], json!(true));
508
+ assert_eq!(out["collected_results"][0]["result_id"], json!("res_collect_red"));
509
+ assert_eq!(out["collected_results"][0]["scope"], json!("task"));
510
+ assert_eq!(
511
+ out["results"],
512
+ json!({"total": 1, "uncollected": 0, "collected": 1, "invalid": 0, "by_status": {}})
513
+ );
514
+ let store = crate::message_store::MessageStore::open(&ws).unwrap();
515
+ let conn = crate::db::schema::open_db(store.db_path()).unwrap();
516
+ let status: String = conn
517
+ .query_row(
518
+ "select status from results where result_id = 'res_collect_red'",
519
+ [],
520
+ |row| row.get(0),
521
+ )
522
+ .unwrap();
523
+ assert_eq!(status, "collected");
524
+ let state = read_state(&ws);
525
+ assert_eq!(state["tasks"][0]["status"], json!("done"));
526
+ assert_eq!(state["tasks"][0]["accepted_result_id"], json!("res_collect_red"));
527
+ assert!(
528
+ read_events(&ws)
529
+ .iter()
530
+ .any(|e| e["event"] == json!("collect.result") && e["result_id"] == json!("res_collect_red")),
531
+ "collect must emit collect.result for the stored result"
532
+ );
533
+ }
534
+ #[test]
535
+ fn stuck_cancel_persists_suppression_and_stuck_list_reads_state() {
536
+ let ws = tmp_workspace();
537
+ seed_collect_state(&ws);
538
+ let out = json_output(
539
+ cmd_stuck_cancel(&StuckCancelArgs {
540
+ agent: "fake_impl".to_string(),
541
+ workspace: ws.clone(),
542
+ alert_type: None,
543
+ json: true,
544
+ })
545
+ .unwrap(),
546
+ );
547
+ let team_key = seeded_team_key(&ws);
548
+ assert_eq!(out["ok"], json!(true));
549
+ assert_eq!(out["alert_types"], json!(["cross_worker_deadlock", "idle_fallback", "stuck"]));
550
+ assert!(out["suppressed"]["idle_fallback"]["snapshot"]["assigned_task_ids"]
551
+ .as_array()
552
+ .unwrap()
553
+ .contains(&json!("task_impl")));
554
+ let state = read_state(&ws);
555
+ assert_eq!(
556
+ state["coordinator"]["suppressed_idle_alerts"][&team_key]["fake_impl"]["idle_fallback"]["suppressed_by"],
557
+ json!("leader")
558
+ );
559
+ assert!(
560
+ read_events(&ws)
561
+ .iter()
562
+ .any(|e| e["event"] == json!("coordinator.idle_alert_suppressed")
563
+ && e["agent_id"] == json!("fake_impl")),
564
+ "stuck_cancel must write coordinator.idle_alert_suppressed"
565
+ );
566
+ let listed = json_output(
567
+ cmd_stuck_list(&StuckListArgs {
568
+ workspace: ws.clone(),
569
+ json: true,
570
+ })
571
+ .unwrap(),
572
+ );
573
+ assert_eq!(
574
+ listed["suppressed_idle_alerts"]["fake_impl"]["stuck"]["suppressed_by"],
575
+ json!("leader"),
576
+ "stuck-list must read the persisted state mirror, not return a hard-coded empty list"
577
+ );
578
+ }
579
+ #[test]
580
+ fn stuck_cancel_invalid_alert_type_is_rejected() {
581
+ let ws = tmp_workspace();
582
+ seed_collect_state(&ws);
583
+ let code = run(
584
+ &cli_argv(&[
585
+ "stuck-cancel",
586
+ "fake_impl",
587
+ "--workspace",
588
+ &ws.to_string_lossy(),
589
+ "--alert-type",
590
+ "bogus",
591
+ "--json",
592
+ ]),
593
+ &ws,
594
+ );
595
+ assert_eq!(
596
+ code,
597
+ ExitCode::Error,
598
+ "Python rejects alert_type outside stuck/idle_fallback/cross_worker_deadlock/all; Rust must not silently coerce bogus to stuck"
599
+ );
600
+ }
601
+ #[test]
602
+ fn acknowledge_idle_records_manual_idle_fallback_suppression_and_event() {
603
+ let ws = tmp_workspace();
604
+ seed_collect_state(&ws);
605
+ let out = json_output(
606
+ cmd_acknowledge_idle(&AcknowledgeIdleArgs {
607
+ team: None,
608
+ workspace: ws.clone(),
609
+ json: true,
610
+ })
611
+ .unwrap(),
612
+ );
613
+ let team_key = seeded_team_key(&ws);
614
+ assert_eq!(out["ok"], json!(true));
615
+ assert_eq!(out["team"], json!(team_key));
616
+ assert_eq!(out["ttl_seconds"], json!(1800));
617
+ let state = read_state(&ws);
618
+ let ack = &state["coordinator"]["idle_acknowledged"][&team_key];
619
+ assert_eq!(ack["ttl_seconds"], json!(1800));
620
+ assert!(ack["acknowledged_at"].as_str().is_some());
621
+ assert_eq!(
622
+ state["coordinator"]["suppressed_idle_alerts"][&team_key]["fake_impl"]["idle_fallback"]["suppressed_by"],
623
+ json!("manual_acknowledge")
624
+ );
625
+ assert_eq!(
626
+ state["coordinator"]["suppressed_idle_alerts"][&team_key]["fake_impl"]["idle_fallback"]["manual_acknowledge"],
627
+ json!(true)
628
+ );
629
+ assert!(
630
+ read_events(&ws)
631
+ .iter()
632
+ .any(|e| e["event"] == json!("coordinator.idle_acknowledged")
633
+ && e["team"] == json!(team_key)
634
+ && e["ttl_seconds"] == json!(1800)),
635
+ "acknowledge-idle must emit coordinator.idle_acknowledged"
636
+ );
637
+ }
638
+ #[test]
639
+ fn repair_state_done_persists_after_status_and_summary() {
640
+ let ws = tmp_workspace();
641
+ seed_collect_state(&ws);
642
+ let out = json_output(
643
+ cmd_repair_state(&RepairStateArgs {
644
+ workspace: ws.clone(),
645
+ task_id: "task_impl".to_string(),
646
+ assignee: None,
647
+ status: "done".to_string(),
648
+ summary: Some("manual repair accepted".to_string()),
649
+ json: true,
650
+ })
651
+ .expect("repair-state should not fail on a valid runtime state"),
652
+ );
653
+ assert_eq!(out["ok"], json!(true));
654
+ assert_eq!(
655
+ out["after"]["status"],
656
+ json!("done"),
657
+ "repair-state --status done must return after.status=done; ok:true with null after fields is a false success"
658
+ );
659
+ assert_eq!(out["after"]["assignee"], json!("fake_impl"));
660
+ assert_eq!(out["after"]["last_result_summary"], json!("manual repair accepted"));
661
+ let state = read_state(&ws);
662
+ let task = state["tasks"]
663
+ .as_array()
664
+ .unwrap()
665
+ .iter()
666
+ .find(|task| task["id"] == json!("task_impl"))
667
+ .unwrap();
668
+ assert_eq!(
669
+ task["status"],
670
+ json!("done"),
671
+ "repair-state --status done must persist the task status, not only emit a success envelope"
672
+ );
673
+ assert_eq!(task["last_result_summary"], json!("manual repair accepted"));
674
+ let _ = std::fs::remove_dir_all(&ws);
675
+ }