@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,263 @@
1
+ use super::*;
2
+
3
+ // ═════════════════════════════════════════════════════════════════════════
4
+ // SPINE SLICE-2a RED — capture-based health-sync obligations (sync_health / refresh_statuses
5
+ // + capture_missing). These are bare record_step() probes today (tick.rs `TODO(spine slice 2):
6
+ // wire via capture seam`), so the daemon does not read live scrollback or update agent status.
7
+ // Golden: coordinator/lifecycle.py → approvals/status.py sync_agent_health /
8
+ // refresh_agent_runtime_statuses (capture pane → classify_agent_activity → activity/status +
9
+ // last_output) and sessions/capture.py capture_missing_sessions (no session_id + transcript →
10
+ // capture_session_id). §11 iron law (bug-071/077/085): an UNKNOWN scrollback is NEVER idle.
11
+ // ═════════════════════════════════════════════════════════════════════════
12
+
13
+ /// The CAPTURE SEAM (test side): a transport whose `capture()` returns SEEDED scrollback, so a test
14
+ /// can stage exactly what a worker's pane shows. The porter wires tick to call transport.capture per
15
+ /// agent. `has_session`→true (gate passes); `inject`→Ok (delivery may run); the rest are Ok defaults.
16
+ struct CapturingTransport {
17
+ scrollback: String,
18
+ }
19
+ impl Transport for CapturingTransport {
20
+ fn kind(&self) -> BackendKind {
21
+ BackendKind::Tmux
22
+ }
23
+ fn spawn_first(&self, _s: &SessionName, _w: &WindowName, _a: &[String], _c: &std::path::Path, _e: &std::collections::BTreeMap<String, String>) -> Result<SpawnResult, TransportError> {
24
+ unimplemented!("not reached")
25
+ }
26
+ fn spawn_into(&self, _s: &SessionName, _w: &WindowName, _a: &[String], _c: &std::path::Path, _e: &std::collections::BTreeMap<String, String>) -> Result<SpawnResult, TransportError> {
27
+ unimplemented!("not reached")
28
+ }
29
+ fn inject(&self, _t: &Target, _p: &InjectPayload, _s: Key, _b: bool) -> Result<InjectReport, TransportError> {
30
+ Ok(InjectReport {
31
+ stage_reached: crate::transport::InjectStage::Submit,
32
+ inject_verification: crate::transport::InjectVerification::CaptureContainsToken,
33
+ submit_verification: crate::transport::SubmitVerification::EnterSentWithoutPlaceholderCheck,
34
+ turn_verification: crate::transport::TurnVerification::NotYetObserved,
35
+ attempts: 1,
36
+ })
37
+ }
38
+ fn send_keys(&self, _t: &Target, _k: &[Key]) -> Result<(), TransportError> {
39
+ Ok(())
40
+ }
41
+ fn capture(&self, _t: &Target, range: CaptureRange) -> Result<CapturedText, TransportError> {
42
+ Ok(CapturedText { text: self.scrollback.clone(), range })
43
+ }
44
+ fn query(&self, _t: &Target, _f: PaneField) -> Result<Option<String>, TransportError> {
45
+ Ok(None)
46
+ }
47
+ fn liveness(&self, _p: &PaneId) -> Result<PaneLiveness, TransportError> {
48
+ Ok(PaneLiveness::Live)
49
+ }
50
+ fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
51
+ Ok(Vec::new())
52
+ }
53
+ fn has_session(&self, _s: &SessionName) -> Result<bool, TransportError> {
54
+ Ok(true)
55
+ }
56
+ fn list_windows(&self, _s: &SessionName) -> Result<Vec<WindowName>, TransportError> {
57
+ Ok(vec![WindowName::new("w1")])
58
+ }
59
+ fn set_session_env(&self, _s: &SessionName, _k: &str, _v: &str) -> Result<SetEnvOutcome, TransportError> {
60
+ Ok(SetEnvOutcome::Applied)
61
+ }
62
+ fn kill_session(&self, _s: &SessionName) -> Result<(), TransportError> {
63
+ Ok(())
64
+ }
65
+ fn kill_window(&self, _t: &Target) -> Result<(), TransportError> {
66
+ Ok(())
67
+ }
68
+ fn attach_session(&self, _s: &SessionName) -> Result<AttachOutcome, TransportError> {
69
+ Ok(AttachOutcome::Attached)
70
+ }
71
+ }
72
+
73
+ /// Build a Coordinator over a real seeded workspace (truthy session_name + the given agents map) with
74
+ /// the CapturingTransport staging `scrollback` for every pane. Returns the workspace dir so the test
75
+ /// can load_runtime_state after the tick.
76
+ fn seeded_health_coord(agents: serde_json::Value, scrollback: &str) -> (Coordinator, std::path::PathBuf) {
77
+ use std::sync::atomic::{AtomicU64, Ordering};
78
+ static N: AtomicU64 = AtomicU64::new(0);
79
+ let dir = std::env::temp_dir().join(format!(
80
+ "ta-rs-health-{}-{}",
81
+ std::process::id(),
82
+ N.fetch_add(1, Ordering::Relaxed)
83
+ ));
84
+ std::fs::create_dir_all(&dir).unwrap();
85
+ crate::state::persist::save_runtime_state(
86
+ &dir,
87
+ &serde_json::json!({ "session_name": "team-health", "agents": agents }),
88
+ )
89
+ .unwrap();
90
+ let ws = WorkspacePath::new(dir.clone());
91
+ let reg: Box<dyn ProviderRegistry> = Box::new(MockRegistry::new(&[], &[]));
92
+ let coord = Coordinator::for_test(
93
+ ws,
94
+ reg,
95
+ Box::new(CapturingTransport { scrollback: scrollback.to_string() }),
96
+ None,
97
+ None,
98
+ );
99
+ (coord, dir)
100
+ }
101
+
102
+ fn agent_activity_status(dir: &std::path::Path, agent: &str) -> Option<String> {
103
+ let state = crate::state::persist::load_runtime_state(dir).ok()?;
104
+ state
105
+ .get("agents")?
106
+ .get(agent)?
107
+ .get("activity")?
108
+ .get("status")?
109
+ .as_str()
110
+ .map(str::to_string)
111
+ }
112
+ fn agent_field(dir: &std::path::Path, agent: &str, field: &str) -> Option<serde_json::Value> {
113
+ let state = crate::state::persist::load_runtime_state(dir).ok()?;
114
+ state.get("agents")?.get(agent)?.get(field).cloned()
115
+ }
116
+
117
+ fn one_agent(provider: &str) -> serde_json::Value {
118
+ serde_json::json!({ "w1": { "provider": provider, "window": "w1", "pane_id": "%1" } })
119
+ }
120
+
121
+ // P0 §11 — an IDLE-prompt scrollback must classify the agent idle (golden classify_agent_activity →
122
+ // state.agents[w1].activity). Today the obligation is a probe → no activity written.
123
+ #[test]
124
+ fn spine2_sync_health_classifies_idle_scrollback() {
125
+ let (coord, dir) = seeded_health_coord(one_agent("codex"), "previous output\n❯\n");
126
+ coord.tick().expect("tick");
127
+ assert_eq!(
128
+ agent_activity_status(&dir, "w1").as_deref(),
129
+ Some("idle"),
130
+ "an idle-prompt scrollback must classify the agent idle (sync_health writes state.agents[w1].activity)"
131
+ );
132
+ }
133
+
134
+ // P0 §11 IRON LAW (bug-071/077/085) — an UNKNOWN/unrecognized scrollback must classify the agent but
135
+ // NEVER as idle. Today: no activity written.
136
+ #[test]
137
+ fn spine2_sync_health_unknown_scrollback_never_idle() {
138
+ let (coord, dir) = seeded_health_coord(one_agent("codex"), "garbled noise xyz 12345 no recognizable signal");
139
+ coord.tick().expect("tick");
140
+ let status = agent_activity_status(&dir, "w1");
141
+ assert!(status.is_some(), "sync_health must classify the agent (write activity); today the probe writes nothing. got {status:?}");
142
+ assert_ne!(status.as_deref(), Some("idle"), "§11: an UNKNOWN scrollback must NEVER be classified idle");
143
+ }
144
+
145
+ // P0 §11 — a WORKING scrollback classifies the agent but never idle.
146
+ #[test]
147
+ fn spine2_sync_health_working_scrollback_never_idle() {
148
+ let (coord, dir) = seeded_health_coord(one_agent("codex"), "Working (5s · esc to interrupt)");
149
+ coord.tick().expect("tick");
150
+ let status = agent_activity_status(&dir, "w1");
151
+ assert!(status.is_some(), "sync_health must classify a working agent; today no activity. got {status:?}");
152
+ assert_ne!(status.as_deref(), Some("idle"), "§11: a WORKING scrollback must not be idle");
153
+ }
154
+
155
+ // P1 — sync_health records last_output_at on a pane delta (so detect_stuck / take-over downstream can
156
+ // use it). Golden approvals/status.py:sync_agent_health. Today: probe writes nothing.
157
+ #[test]
158
+ fn spine2_sync_health_records_last_output_at() {
159
+ let (coord, dir) = seeded_health_coord(one_agent("codex"), "some fresh pane output");
160
+ coord.tick().expect("tick");
161
+ assert!(
162
+ agent_field(&dir, "w1", "last_output_at").is_some(),
163
+ "sync_health must record last_output_at on a pane delta; today the probe writes nothing"
164
+ );
165
+ }
166
+
167
+ // P1 — capture_missing: an agent with NO session_id but a discoverable transcript under spawn_cwd gets
168
+ // its session_id captured + persisted (real capture_session_id); an agent that already has one is
169
+ // untouched. Golden sessions/capture.py:capture_missing_sessions.
170
+ #[test]
171
+ fn spine2_capture_missing_captures_session_id_for_missing_agent() {
172
+ use std::sync::atomic::{AtomicU64, Ordering};
173
+ static N: AtomicU64 = AtomicU64::new(0);
174
+ let tdir = std::env::temp_dir().join(format!("ta-rs-health-tx-{}-{}", std::process::id(), N.fetch_add(1, Ordering::Relaxed)));
175
+ std::fs::create_dir_all(&tdir).unwrap();
176
+ std::fs::write(
177
+ tdir.join("session.jsonl"),
178
+ r#"{"type":"user","sessionId":"sess-found","cwd":"x","message":{"content":"hi"}}"#,
179
+ )
180
+ .unwrap();
181
+ let agents = serde_json::json!({
182
+ "w1": { "provider": "claude_code", "window": "w1", "spawn_cwd": tdir.to_string_lossy() },
183
+ "w2": { "provider": "claude_code", "window": "w2", "session_id": "existing-sess" },
184
+ });
185
+ let (coord, dir) = seeded_health_coord(agents, "");
186
+ coord.tick().expect("tick");
187
+ assert!(
188
+ agent_field(&dir, "w1", "session_id").and_then(|v| v.as_str().map(str::to_string)).is_some(),
189
+ "capture_missing must capture+persist a session_id for an agent with a discoverable transcript; today it's a probe"
190
+ );
191
+ assert_eq!(
192
+ agent_field(&dir, "w2", "session_id").and_then(|v| v.as_str().map(str::to_string)).as_deref(),
193
+ Some("existing-sess"),
194
+ "an agent that already has a session_id must be untouched"
195
+ );
196
+ }
197
+
198
+ // CONTRACT — sync_health runs BEFORE deliver_pending, but turn-level WORKING state must not make an
199
+ // alive worker undeliverable. Busy delivery deferral is lifecycle-only (`state.agents[id].status=="busy"`);
200
+ // activity/agent_health WORKING remains diagnostic turn state.
201
+ #[test]
202
+ fn spine2_sync_health_working_status_delivers_same_tick() {
203
+ let (coord, dir) = seeded_health_coord(one_agent("codex"), "Working (5s · esc to interrupt)");
204
+ let store = MessageStore::open(&dir).unwrap();
205
+ let mid = store.create_message(Some("t"), "leader", "w1", "hi", None, true, None).unwrap();
206
+ let conn = crate::db::schema::open_db(store.db_path()).unwrap();
207
+ conn.execute(
208
+ "insert into agent_health(owner_team_id, agent_id, status, updated_at)
209
+ values (?1, ?2, ?3, ?4)
210
+ on conflict(owner_team_id, agent_id) do update set
211
+ status = excluded.status,
212
+ updated_at = excluded.updated_at",
213
+ rusqlite::params!["current", "w1", "WORKING", chrono::Utc::now().to_rfc3339()],
214
+ )
215
+ .unwrap();
216
+ drop(store);
217
+ coord.tick().expect("tick");
218
+ let events = read_event_log_dir(&dir);
219
+ assert!(
220
+ !events.iter().any(|e| e.get("event").and_then(|v| v.as_str()) == Some("send.deferred_busy")),
221
+ "turn-level WORKING must not trigger lifecycle busy deferral; got {events:?}"
222
+ );
223
+ assert!(
224
+ events.iter().any(|e| {
225
+ e.get("event").and_then(|v| v.as_str()) == Some("message.delivered")
226
+ && e.get("message_id").and_then(|v| v.as_str()) == Some(mid.as_str())
227
+ }),
228
+ "alive worker with WORKING turn state must still receive the pending message; got {events:?}"
229
+ );
230
+ }
231
+
232
+ // ADVERSARIAL (real-machine-driven; catches porter fix 1f97163 re-coupling): a worker classified WORKING
233
+ // by sync_health is STILL ALIVE (lifecycle status running) and MUST remain deliverable. golden never maps
234
+ // turn activity to lifecycle status (status is only running/stopped; the status=="busy" gate is vestigial/
235
+ // unreachable — golden delivers to alive workers regardless of activity). The porter fix (write_activity,
236
+ // tick.rs:858) maps activity=Working -> status="busy", re-coupling turn state into lifecycle status — it
237
+ // just MOVED the deferral from agent_health to status, re-introducing the regression (fake workers are
238
+ // permanently WORKING -> permanently status=busy -> permanently deferred -> round-trip never closes).
239
+ // The synthetic contract REDs seed status=running directly and skip the tick, so they don't catch this;
240
+ // this drives the REAL coordinator tick. (Contradicts the stale lock
241
+ // spine2_sync_health_busy_status_defers_delivery_same_tick, which encodes the regression behavior and
242
+ // must be reconciled to this contract.)
243
+ #[test]
244
+ fn contract_working_worker_stays_alive_and_deliverable_in_real_tick() {
245
+ let (coord, dir) = seeded_health_coord(one_agent("codex"), "Working (5s · esc to interrupt)");
246
+ let store = MessageStore::open(&dir).unwrap();
247
+ let _mid = store.create_message(Some("t"), "leader", "w1", "hi", None, true, None).unwrap();
248
+ drop(store);
249
+ coord.tick().expect("tick");
250
+ let status = agent_field(&dir, "w1", "status").and_then(|v| v.as_str().map(str::to_string));
251
+ assert_ne!(
252
+ status.as_deref(),
253
+ Some("busy"),
254
+ "CONTRACT: sync_health must NOT write lifecycle status='busy' from turn activity=Working (golden never \
255
+ maps activity->status); turn state belongs in agent['activity']/agent_health only. got status={status:?}"
256
+ );
257
+ let events = read_event_log_dir(&dir);
258
+ assert!(
259
+ !events.iter().any(|e| e.get("event").and_then(|v| v.as_str()) == Some("send.deferred_busy")),
260
+ "CONTRACT: an alive worker (lifecycle running) classified WORKING must still receive delivery, not \
261
+ deferred_busy (golden delivers; fake workers are permanently WORKING). got {events:?}"
262
+ );
263
+ }
@@ -0,0 +1,136 @@
1
+ use super::*;
2
+ use super::spine::{message_status, DeliveringTransport};
3
+
4
+ // ═════════════════════════════════════════════════════════════════════════
5
+ // P0 REGRESSION (root cause pinned) — a per-agent capture/health-check FAILURE must not abort the
6
+ // whole tick. rt-host-a baseline @ ea4ba97: after `stop-agent w1`, `send w2` stays status='accepted'
7
+ // with delivery_attempts=0 while the coordinator stays alive. Root cause: sync_agent_health
8
+ // (tick.rs) does `self.transport.capture(&target, ...)?` per agent — when the stopped w1's window is
9
+ // gone the capture ERRORS and the `?` propagates -> sync_agent_health returns Err -> the tick
10
+ // early-returns Err BEFORE deliver_pending_messages -> the deliver loop never runs for the active w2.
11
+ // Contract: the tick must SWALLOW a per-agent capture/health failure (log + continue) and still run
12
+ // deliver_pending_messages for the other agents. Unit repro: a transport whose capture() ERRORS
13
+ // (window gone) but whose session exists + inject delivers -> the tick must (a) NOT return Err, and
14
+ // (b) still attempt delivery to the active w2 (delivery_attempts +1 / status advances past 'accepted').
15
+ // ═════════════════════════════════════════════════════════════════════════
16
+ fn message_delivery_attempts(dir: &std::path::Path, message_id: &str) -> i64 {
17
+ let store = MessageStore::open(dir).unwrap();
18
+ let conn = crate::db::schema::open_db(store.db_path()).unwrap();
19
+ conn.query_row(
20
+ "select delivery_attempts from messages where message_id = ?1",
21
+ [message_id],
22
+ |r| r.get::<_, i64>(0),
23
+ )
24
+ .unwrap()
25
+ }
26
+ /// A transport that DELIVERS (inject ok, session present) but whose `capture` ALWAYS fails — modelling a
27
+ /// stopped agent whose tmux window is gone. Exercises sync_agent_health's per-agent `capture?`: today the
28
+ /// error propagates and aborts the whole tick; the fix must swallow it and continue to deliver.
29
+ struct CaptureFailsDeliverTransport {
30
+ inner: DeliveringTransport,
31
+ }
32
+ impl CaptureFailsDeliverTransport {
33
+ fn new() -> Self {
34
+ Self { inner: DeliveringTransport::new() }
35
+ }
36
+ }
37
+ impl Transport for CaptureFailsDeliverTransport {
38
+ fn kind(&self) -> BackendKind {
39
+ self.inner.kind()
40
+ }
41
+ fn spawn_first(&self, s: &SessionName, w: &WindowName, a: &[String], c: &std::path::Path, e: &std::collections::BTreeMap<String, String>) -> Result<SpawnResult, TransportError> {
42
+ self.inner.spawn_first(s, w, a, c, e)
43
+ }
44
+ fn spawn_into(&self, s: &SessionName, w: &WindowName, a: &[String], c: &std::path::Path, e: &std::collections::BTreeMap<String, String>) -> Result<SpawnResult, TransportError> {
45
+ self.inner.spawn_into(s, w, a, c, e)
46
+ }
47
+ fn inject(&self, t: &Target, p: &InjectPayload, submit: Key, bracketed: bool) -> Result<InjectReport, TransportError> {
48
+ self.inner.inject(t, p, submit, bracketed)
49
+ }
50
+ fn send_keys(&self, t: &Target, k: &[Key]) -> Result<(), TransportError> {
51
+ self.inner.send_keys(t, k)
52
+ }
53
+ fn capture(&self, _t: &Target, _r: CaptureRange) -> Result<CapturedText, TransportError> {
54
+ // the agent's window is gone — capture fails (tmux can't find the target).
55
+ Err(TransportError::TargetNotFound { target: "window gone (stopped agent)".to_string() })
56
+ }
57
+ fn query(&self, t: &Target, f: PaneField) -> Result<Option<String>, TransportError> {
58
+ self.inner.query(t, f)
59
+ }
60
+ fn liveness(&self, p: &PaneId) -> Result<PaneLiveness, TransportError> {
61
+ self.inner.liveness(p)
62
+ }
63
+ fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
64
+ self.inner.list_targets()
65
+ }
66
+ fn has_session(&self, s: &SessionName) -> Result<bool, TransportError> {
67
+ self.inner.has_session(s)
68
+ }
69
+ fn list_windows(&self, s: &SessionName) -> Result<Vec<WindowName>, TransportError> {
70
+ self.inner.list_windows(s)
71
+ }
72
+ fn set_session_env(&self, s: &SessionName, k: &str, v: &str) -> Result<SetEnvOutcome, TransportError> {
73
+ self.inner.set_session_env(s, k, v)
74
+ }
75
+ fn kill_session(&self, s: &SessionName) -> Result<(), TransportError> {
76
+ self.inner.kill_session(s)
77
+ }
78
+ fn kill_window(&self, t: &Target) -> Result<(), TransportError> {
79
+ self.inner.kill_window(t)
80
+ }
81
+ fn attach_session(&self, s: &SessionName) -> Result<AttachOutcome, TransportError> {
82
+ self.inner.attach_session(s)
83
+ }
84
+ }
85
+ #[test]
86
+ fn tick_swallows_capture_failure_and_still_delivers_to_other_agent() {
87
+ let dir = std::env::temp_dir().join(format!(
88
+ "team-agent-coord-stopreg-{}-{}",
89
+ std::process::id(),
90
+ std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()
91
+ ));
92
+ std::fs::create_dir_all(&dir).unwrap();
93
+ // w1 STOPPED (window gone -> capture fails), w2 ACTIVE — w2 keeps the tmux session alive.
94
+ crate::state::persist::save_runtime_state(
95
+ &dir,
96
+ &serde_json::json!({
97
+ "session_name": "team-spine",
98
+ "agents": {
99
+ "w1": { "provider": "codex", "status": "stopped", "window": "w1" },
100
+ "w2": { "provider": "codex", "status": "running", "window": "w2" }
101
+ }
102
+ }),
103
+ )
104
+ .unwrap();
105
+ let store = MessageStore::open(&dir).unwrap();
106
+ let mid = store
107
+ .create_message(Some("task-1"), "leader", "w2", "after stop", None, true, None)
108
+ .unwrap();
109
+ drop(store);
110
+ assert_eq!(message_status(&dir, &mid), "accepted", "precondition: a fresh message is 'accepted'");
111
+ let ws = WorkspacePath::new(dir.clone());
112
+ let reg: Box<dyn ProviderRegistry> = Box::new(MockRegistry::new(&[], &[]));
113
+ // capture() ERRORS (window gone) but inject delivers + session present.
114
+ let coord = Coordinator::for_test(ws, reg, Box::new(CaptureFailsDeliverTransport::new()), None, None);
115
+ let result = coord.tick();
116
+ // (a) a per-agent capture failure must NOT abort the whole tick.
117
+ assert!(
118
+ result.is_ok(),
119
+ "P0: sync_agent_health's per-agent `capture?` must be SWALLOWED (log + continue), not propagated — \
120
+ the tick must NOT early-return Err when a stopped agent's window-capture fails; got {result:?}"
121
+ );
122
+ let report = result.unwrap();
123
+ // (b) the deliver loop must still run for the active w2.
124
+ let attempts = message_delivery_attempts(&dir, &mid);
125
+ let delivered_reported = report.delivered.iter().any(|d| d.message_id == mid);
126
+ let advanced = message_status(&dir, &mid) != "accepted";
127
+ assert!(
128
+ attempts >= 1 || delivered_reported || advanced,
129
+ "P0: after swallowing w1's capture failure, the tick MUST still deliver to the active w2 — its \
130
+ message must advance past 'accepted' (delivery_attempts +1 / claimed / delivered). The regression \
131
+ halts before deliver_pending_messages and leaves it at delivery_attempts=0. attempts={attempts} \
132
+ status={} delivered={:?}",
133
+ message_status(&dir, &mid),
134
+ report.delivered
135
+ );
136
+ }