@team-agent/installer 0.2.11 → 0.3.1

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 +1204 -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 +1207 -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 +557 -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 +1084 -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 +526 -0
  59. package/crates/team-agent/src/leader/rediscover.rs +1101 -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 +237 -0
  64. package/crates/team-agent/src/leader/tests/identity.rs +206 -0
  65. package/crates/team-agent/src/leader/tests/idle.rs +272 -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 +410 -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 +489 -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 +2109 -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 +985 -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 +710 -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 +187 -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 +468 -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 +743 -0
  110. package/crates/team-agent/src/messaging/helpers.rs +209 -0
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +329 -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 +553 -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 +578 -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 +659 -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 +765 -0
  172. package/crates/team-agent/src/tmux_backend.rs +810 -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 +118 -112
  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,557 @@
1
+ //! coordinator 健康/身份 & 只读可观测面:metadata 身份原语 + coordinator 路径 + watch 实时流。
2
+
3
+ use std::io::{Read, Seek, SeekFrom};
4
+ use std::path::{Path, PathBuf};
5
+ use std::process::{Command, Stdio};
6
+ use std::time::Duration;
7
+
8
+ use serde_json::Value;
9
+ use thiserror::Error;
10
+
11
+ use crate::message_store::MessageStore;
12
+
13
+ use super::types::{
14
+ CoordinatorHealthStatus, CoordinatorMetadata, HealthReport, MetadataSource, Pid, SchemaError,
15
+ SchemaHealth, StartError, StartOutcome, StartReport, StopError, StopOutcome, StopReport,
16
+ WatchCursor, WorkspacePath, PROTOCOL_VERSION, ROTATION_MARKER,
17
+ };
18
+
19
+ // ===========================================================================
20
+ // coordinator daemon lifecycle (lifecycle.py:38-247).
21
+ // start_coordinator spawns the `team-agent coordinator --workspace <ws>` daemon subprocess;
22
+ // the actual spawn is the #[ignore] real-machine boundary, the idempotent decision is testable.
23
+ // ===========================================================================
24
+
25
+ /// `coordinator_health`(`lifecycle.py:38-46`):`running ∧ metadata_ok ∧ schema_ok` → typed report.
26
+ pub fn coordinator_health(workspace: &WorkspacePath) -> HealthReport {
27
+ let schema = message_store_schema_health(workspace);
28
+ let pid_path = coordinator_pid_path(workspace);
29
+ let pid = read_pid_file(&pid_path);
30
+ let status = match pid {
31
+ Some(pid) => match pid_is_running(pid) {
32
+ Ok(true) => CoordinatorHealthStatus::Running,
33
+ Ok(false) | Err(_) => CoordinatorHealthStatus::Stale,
34
+ },
35
+ None if pid_path.exists() => CoordinatorHealthStatus::InvalidPid,
36
+ None => CoordinatorHealthStatus::Missing,
37
+ };
38
+ let metadata = read_coordinator_metadata(workspace);
39
+ let metadata_ok = pid.is_some_and(|p| coordinator_metadata_ok(metadata.as_ref(), p));
40
+ let running = matches!(status, CoordinatorHealthStatus::Running);
41
+ HealthReport {
42
+ ok: running && metadata_ok && schema.ok,
43
+ status,
44
+ pid,
45
+ metadata,
46
+ metadata_ok,
47
+ schema,
48
+ }
49
+ }
50
+
51
+ /// `start_coordinator`(`lifecycle.py:49-121`):幂等 — 已健康 no-op(AlreadyRunning);metadata 不兼容
52
+ /// 先 stop 再起;schema 不兼容拒启 + hint;否则 spawn `team-agent coordinator --workspace <ws>`。
53
+ pub fn start_coordinator(workspace: &WorkspacePath) -> Result<StartReport, StartError> {
54
+ let health = coordinator_health(workspace);
55
+ if health.ok {
56
+ return Ok(StartReport {
57
+ ok: true,
58
+ pid: health.pid,
59
+ status: StartOutcome::AlreadyRunning,
60
+ log: Some(coordinator_log_path(workspace)),
61
+ schema_error: None,
62
+ action: None,
63
+ });
64
+ }
65
+ if !health.schema.ok {
66
+ return Ok(StartReport {
67
+ ok: false,
68
+ pid: health.pid,
69
+ status: StartOutcome::SchemaIncompatible,
70
+ log: None,
71
+ schema_error: health.schema.error,
72
+ action: health.schema.action,
73
+ });
74
+ }
75
+ if health.pid.is_some() && !health.metadata_ok && health.metadata.is_some() {
76
+ match stop_coordinator(workspace) {
77
+ Ok(stop) if stop.ok => {}
78
+ Ok(_) | Err(_) => {
79
+ return Ok(StartReport {
80
+ ok: false,
81
+ pid: health.pid,
82
+ status: StartOutcome::RestartIncompatibleStopFailed,
83
+ log: None,
84
+ schema_error: None,
85
+ action: None,
86
+ });
87
+ }
88
+ }
89
+ }
90
+
91
+ let runtime_dir = crate::model::paths::runtime_dir(workspace.as_path());
92
+ std::fs::create_dir_all(&runtime_dir)?;
93
+ let log_path = coordinator_log_path(workspace);
94
+ let log = std::fs::OpenOptions::new()
95
+ .create(true)
96
+ .append(true)
97
+ .open(&log_path)?;
98
+ let log_err = log.try_clone()?;
99
+ let child = Command::new(std::env::current_exe()?)
100
+ .args(["coordinator", "--workspace"])
101
+ .arg(workspace.as_path())
102
+ .stdin(Stdio::null())
103
+ .stdout(Stdio::from(log))
104
+ .stderr(Stdio::from(log_err))
105
+ .spawn()?;
106
+ let pid = Pid::new(child.id());
107
+ std::fs::write(coordinator_pid_path(workspace), pid.to_string())?;
108
+ write_coordinator_metadata(workspace, pid, MetadataSource::Start)?;
109
+ Ok(StartReport {
110
+ ok: true,
111
+ pid: Some(pid),
112
+ status: StartOutcome::Started,
113
+ log: Some(log_path),
114
+ schema_error: None,
115
+ action: None,
116
+ })
117
+ }
118
+
119
+ /// `stop_coordinator`(`lifecycle.py:228-247`):SIGTERM pid + 清 pid/meta → typed report。
120
+ pub fn stop_coordinator(workspace: &WorkspacePath) -> Result<StopReport, StopError> {
121
+ let pid_path = coordinator_pid_path(workspace);
122
+ if !pid_path.exists() {
123
+ if let Some(report) = stop_discovered_coordinators(workspace)? {
124
+ return Ok(report);
125
+ }
126
+ return Ok(StopReport {
127
+ ok: true,
128
+ status: StopOutcome::Missing,
129
+ pid: None,
130
+ });
131
+ }
132
+ let Some(pid) = read_pid_file(&pid_path) else {
133
+ remove_file_if_exists(&pid_path)?;
134
+ remove_file_if_exists(&coordinator_meta_path(workspace))?;
135
+ return Ok(StopReport {
136
+ ok: true,
137
+ status: StopOutcome::InvalidPidRemoved,
138
+ pid: None,
139
+ });
140
+ };
141
+ let Ok(pid_t) = libc::pid_t::try_from(pid.get()) else {
142
+ return Ok(StopReport {
143
+ ok: false,
144
+ status: StopOutcome::KillFailed,
145
+ pid: Some(pid),
146
+ });
147
+ };
148
+ let rc = unsafe { libc::kill(pid_t, libc::SIGTERM) };
149
+ if rc != 0 {
150
+ return Ok(StopReport {
151
+ ok: false,
152
+ status: StopOutcome::KillFailed,
153
+ pid: Some(pid),
154
+ });
155
+ }
156
+ remove_file_if_exists(&pid_path)?;
157
+ remove_file_if_exists(&coordinator_meta_path(workspace))?;
158
+ Ok(StopReport {
159
+ ok: true,
160
+ status: StopOutcome::Stopped,
161
+ pid: Some(pid),
162
+ })
163
+ }
164
+
165
+ fn stop_discovered_coordinators(
166
+ workspace: &WorkspacePath,
167
+ ) -> Result<Option<StopReport>, StopError> {
168
+ let pids = discover_coordinator_pids(workspace);
169
+ if pids.is_empty() {
170
+ return Ok(None);
171
+ }
172
+
173
+ let mut stopped = None;
174
+ let mut failed = None;
175
+ for pid in pids {
176
+ if terminate_pid(pid) {
177
+ stopped.get_or_insert(pid);
178
+ } else {
179
+ failed.get_or_insert(pid);
180
+ }
181
+ }
182
+ remove_file_if_exists(&coordinator_meta_path(workspace))?;
183
+
184
+ if let Some(pid) = stopped {
185
+ Ok(Some(StopReport {
186
+ ok: true,
187
+ status: StopOutcome::Stopped,
188
+ pid: Some(pid),
189
+ }))
190
+ } else {
191
+ Ok(Some(StopReport {
192
+ ok: false,
193
+ status: StopOutcome::KillFailed,
194
+ pid: failed,
195
+ }))
196
+ }
197
+ }
198
+
199
+ fn discover_coordinator_pids(workspace: &WorkspacePath) -> Vec<Pid> {
200
+ let output = match Command::new("ps")
201
+ .args(["-axo", "pid=,command="])
202
+ .output()
203
+ {
204
+ Ok(output) if output.status.success() => output,
205
+ _ => return Vec::new(),
206
+ };
207
+ let text = String::from_utf8_lossy(&output.stdout);
208
+ let candidates = workspace_match_candidates(workspace.as_path());
209
+ text.lines()
210
+ .filter_map(|line| parse_ps_command_line(line))
211
+ .filter(|(pid, command)| {
212
+ *pid != std::process::id()
213
+ && coordinator_command_matches_workspace(command, &candidates)
214
+ })
215
+ .map(|(pid, _)| Pid::new(pid))
216
+ .collect()
217
+ }
218
+
219
+ fn parse_ps_command_line(line: &str) -> Option<(u32, &str)> {
220
+ let line = line.trim_start();
221
+ let split = line
222
+ .find(char::is_whitespace)
223
+ .unwrap_or(line.len());
224
+ let pid = line.get(..split)?.trim().parse::<u32>().ok()?;
225
+ let command = line.get(split..)?.trim();
226
+ Some((pid, command))
227
+ }
228
+
229
+ fn workspace_match_candidates(workspace: &Path) -> Vec<String> {
230
+ let mut candidates = vec![workspace.to_string_lossy().to_string()];
231
+ if let Ok(canonical) = workspace.canonicalize() {
232
+ let text = canonical.to_string_lossy().to_string();
233
+ if !candidates.iter().any(|candidate| candidate == &text) {
234
+ candidates.push(text);
235
+ }
236
+ }
237
+ candidates
238
+ }
239
+
240
+ fn coordinator_command_matches_workspace(command: &str, workspaces: &[String]) -> bool {
241
+ command
242
+ .split_whitespace()
243
+ .any(|token| token == "team-agent" || token.ends_with("/team-agent"))
244
+ && command.split_whitespace().any(|token| token == "coordinator")
245
+ && command.contains("--workspace")
246
+ && workspaces.iter().any(|workspace| command.contains(workspace))
247
+ }
248
+
249
+ fn terminate_pid(pid: Pid) -> bool {
250
+ if pid_is_running(pid).ok() == Some(false) {
251
+ return true;
252
+ }
253
+ if !send_signal(pid, libc::SIGTERM) {
254
+ return false;
255
+ }
256
+ if wait_until_not_running(pid, Duration::from_millis(750)) {
257
+ return true;
258
+ }
259
+ send_signal(pid, libc::SIGKILL) && wait_until_not_running(pid, Duration::from_millis(750))
260
+ }
261
+
262
+ fn send_signal(pid: Pid, signal: libc::c_int) -> bool {
263
+ let Ok(pid_t) = libc::pid_t::try_from(pid.get()) else {
264
+ return false;
265
+ };
266
+ unsafe { libc::kill(pid_t, signal) == 0 }
267
+ }
268
+
269
+ fn wait_until_not_running(pid: Pid, timeout: Duration) -> bool {
270
+ let start = std::time::Instant::now();
271
+ loop {
272
+ if pid_is_running(pid).ok() != Some(true) {
273
+ return true;
274
+ }
275
+ if start.elapsed() >= timeout {
276
+ return false;
277
+ }
278
+ std::thread::sleep(Duration::from_millis(25));
279
+ }
280
+ }
281
+
282
+ // ===========================================================================
283
+ // metadata 身份原语(metadata.py)—— 自由函数面
284
+ // ===========================================================================
285
+
286
+ /// `pid_is_running`(`metadata.py:16-25`):`os.kill(pid, 0)` + `ps -o stat=` 查 zombie(Z* → 不算活)。
287
+ /// §10 fallible:进程探测 I/O 可失败 → Result。
288
+ pub fn pid_is_running(pid: Pid) -> Result<bool, std::io::Error> {
289
+ let Ok(pid_t) = libc::pid_t::try_from(pid.get()) else {
290
+ return Ok(false);
291
+ };
292
+ let signal_rc = unsafe { libc::kill(pid_t, 0) };
293
+ if signal_rc != 0 {
294
+ let err = std::io::Error::last_os_error();
295
+ return match err.raw_os_error() {
296
+ Some(libc::EPERM) | Some(libc::ESRCH) => Ok(false),
297
+ _ => Err(err),
298
+ };
299
+ }
300
+ let out = Command::new("ps")
301
+ .args(["-p", &pid.to_string(), "-o", "stat="])
302
+ .output()?;
303
+ if !out.status.success() {
304
+ return Ok(false);
305
+ }
306
+ let stat = String::from_utf8_lossy(&out.stdout).trim().to_string();
307
+ Ok(!stat.is_empty() && !stat.starts_with('Z'))
308
+ }
309
+
310
+ /// `read_coordinator_metadata`(`metadata.py:28-34`)。读 `coordinator.json`;损坏/缺失/非 dict → `None`。
311
+ pub fn read_coordinator_metadata(workspace: &WorkspacePath) -> Option<CoordinatorMetadata> {
312
+ let text = std::fs::read_to_string(coordinator_meta_path(workspace)).ok()?;
313
+ serde_json::from_str(&text).ok()
314
+ }
315
+
316
+ /// `coordinator_metadata_ok`(`metadata.py:37-43`):三元全等
317
+ /// `meta.pid == pid ∧ meta.protocol_version == PROTOCOL_VERSION ∧
318
+ /// meta.message_store_schema_version == SCHEMA_VERSION`。任一不符 → false(不静默继续旧 schema)。
319
+ pub fn coordinator_metadata_ok(metadata: Option<&CoordinatorMetadata>, pid: Pid) -> bool {
320
+ metadata.is_some_and(|m| {
321
+ m.pid == pid
322
+ && m.protocol_version == PROTOCOL_VERSION
323
+ && m.message_store_schema_version == crate::db::schema::SCHEMA_VERSION
324
+ })
325
+ }
326
+
327
+ /// `write_coordinator_metadata`(`metadata.py:46-61`)。写 `coordinator.json`(pretty indent=2),
328
+ /// `updated_at = now(utc).isoformat()`。
329
+ pub fn write_coordinator_metadata(
330
+ workspace: &WorkspacePath,
331
+ pid: Pid,
332
+ source: MetadataSource,
333
+ ) -> Result<(), std::io::Error> {
334
+ let path = coordinator_meta_path(workspace);
335
+ if let Some(parent) = path.parent() {
336
+ std::fs::create_dir_all(parent)?;
337
+ }
338
+ let metadata = CoordinatorMetadata {
339
+ pid,
340
+ protocol_version: PROTOCOL_VERSION,
341
+ message_store_schema_version: crate::db::schema::SCHEMA_VERSION,
342
+ source,
343
+ updated_at: chrono::Utc::now().to_rfc3339(),
344
+ };
345
+ let text = serde_json::to_string_pretty(&metadata)
346
+ .map_err(|e| std::io::Error::other(e.to_string()))?;
347
+ std::fs::write(path, text)
348
+ }
349
+
350
+ fn message_store_schema_health(workspace: &WorkspacePath) -> SchemaHealth {
351
+ match MessageStore::open(workspace.as_path()) {
352
+ Ok(_) => SchemaHealth {
353
+ ok: true,
354
+ schema_version: crate::db::schema::SCHEMA_VERSION,
355
+ error: None,
356
+ action: None,
357
+ },
358
+ Err(e) => SchemaHealth {
359
+ ok: false,
360
+ schema_version: crate::db::schema::SCHEMA_VERSION,
361
+ error: Some(SchemaError::InitFailed {
362
+ message: e.to_string(),
363
+ }),
364
+ action: Some("run team-agent repair-state --schema".to_string()),
365
+ },
366
+ }
367
+ }
368
+
369
+ fn read_pid_file(path: &Path) -> Option<Pid> {
370
+ let text = std::fs::read_to_string(path).ok()?;
371
+ let raw = text.trim().parse::<u32>().ok()?;
372
+ Some(Pid::new(raw))
373
+ }
374
+
375
+ fn remove_file_if_exists(path: &Path) -> Result<(), std::io::Error> {
376
+ match std::fs::remove_file(path) {
377
+ Ok(()) => Ok(()),
378
+ Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
379
+ Err(e) => Err(e),
380
+ }
381
+ }
382
+
383
+ // ===========================================================================
384
+ // coordinator 路径(paths.py)
385
+ // ===========================================================================
386
+
387
+ /// `coordinator.pid` 路径(`paths.py:8`)= `runtime_dir(workspace)/coordinator.pid`。
388
+ pub fn coordinator_pid_path(workspace: &WorkspacePath) -> PathBuf {
389
+ crate::model::paths::runtime_dir(workspace.as_path()).join("coordinator.pid")
390
+ }
391
+
392
+ /// `coordinator.json` 路径(`paths.py:12`)。
393
+ pub fn coordinator_meta_path(workspace: &WorkspacePath) -> PathBuf {
394
+ crate::model::paths::runtime_dir(workspace.as_path()).join("coordinator.json")
395
+ }
396
+
397
+ /// `coordinator.log` 路径(`paths.py:16`)。
398
+ pub fn coordinator_log_path(workspace: &WorkspacePath) -> PathBuf {
399
+ crate::model::paths::runtime_dir(workspace.as_path()).join("coordinator.log")
400
+ }
401
+
402
+ // ===========================================================================
403
+ // watch 实时流(watch/__init__.py)—— `team-agent watch`
404
+ // ===========================================================================
405
+
406
+ /// `collect_watch_lines`(`watch.py:40`)。tail events.jsonl(过滤 team)+ latest_results,
407
+ /// 渲染人类可读行;处理 log rotation(ROTATION_MARKER + offset 重置,不重放历史段)。
408
+ /// 推进 `cursor`。
409
+ pub fn collect_watch_lines(
410
+ workspace: &WorkspacePath,
411
+ cursor: &mut WatchCursor,
412
+ store: &MessageStore,
413
+ team: Option<&str>,
414
+ ) -> Result<Vec<String>, WatchError> {
415
+ let _ = (store, team);
416
+ let logs = crate::model::paths::logs_dir(workspace.as_path());
417
+ let events_path = logs.join("events.jsonl");
418
+ let archive_path = logs.join("events.jsonl.1");
419
+ let archive_signature = file_signature(&archive_path)?;
420
+ let mut lines = Vec::new();
421
+
422
+ let size = std::fs::metadata(&events_path).map(|m| m.len()).unwrap_or(0);
423
+ let rotated = cursor.initialized
424
+ && (cursor.archive_signature != archive_signature || cursor.event_offset > size);
425
+ if rotated {
426
+ lines.push(ROTATION_MARKER.to_string());
427
+ cursor.event_offset = 0;
428
+ }
429
+ cursor.archive_signature = archive_signature;
430
+
431
+ let mut file = match std::fs::File::open(&events_path) {
432
+ Ok(file) => file,
433
+ Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
434
+ cursor.initialized = true;
435
+ return Ok(lines);
436
+ }
437
+ Err(e) => return Err(WatchError::Io(e)),
438
+ };
439
+ file.seek(SeekFrom::Start(cursor.event_offset))?;
440
+ let mut text = String::new();
441
+ file.read_to_string(&mut text)?;
442
+ cursor.event_offset = file.stream_position()?;
443
+ cursor.initialized = true;
444
+ for line in text.lines() {
445
+ if let Ok(event) = serde_json::from_str::<Value>(line) {
446
+ if let Some(rendered) = render_event_line(&event) {
447
+ lines.push(rendered);
448
+ }
449
+ }
450
+ }
451
+ Ok(lines)
452
+ }
453
+
454
+ /// `render_event_line`(`watch.py:46-63`)。把一条 step 4 事件渲染成人类可读行;非可渲染事件 → `None`。
455
+ /// 消费的事件类型:`result_received` / `leader_receiver.{injected,submitted}` / `send.failed` /
456
+ /// `leader_receiver.rebind_required` / `leader.api_error`(card 表)。
457
+ pub fn render_event_line(event: &Value) -> Option<String> {
458
+ let event_name = event.get("event").and_then(Value::as_str)?;
459
+ match event_name {
460
+ "result_received" => Some(format!(
461
+ "result_received: {} -> {}",
462
+ clean_field(event, &["agent_id"], "-"),
463
+ prefix_chars(&clean_field(event, &["summary"], "-"), 80)
464
+ )),
465
+ "leader_receiver.injected" | "leader_receiver.submitted" => {
466
+ let id = first_field(event, &["message_id", "msg_id"]).unwrap_or("-");
467
+ let id = prefix_chars(id, 12);
468
+ Some(format!(
469
+ "leader_receiver.injected: {} -> {}",
470
+ id,
471
+ clean_field(event, &["recipient", "to"], "-")
472
+ ))
473
+ }
474
+ "send.failed" => Some(format!(
475
+ "send.failed: {} reason={}",
476
+ clean_field(event, &["recipient", "to", "target"], "-"),
477
+ clean_field(event, &["reason", "error"], "-")
478
+ )),
479
+ "leader_receiver.rebind_required" => Some(format!(
480
+ "leader_receiver.rebind_required: pane={} reason={}",
481
+ clean_field(event, &["old_pane_id", "pane_id", "target"], "-"),
482
+ clean_field(event, &["reason", "rediscovery_status"], "-")
483
+ )),
484
+ "leader.api_error" => Some(format!(
485
+ "leader.api_error: {} provider={} snippet={}",
486
+ clean_field(event, &["error_class"], "Unknown"),
487
+ clean_field(event, &["provider"], "-"),
488
+ clean_field(event, &["matched_pattern_snippet", "snippet"], "-")
489
+ )),
490
+ _ => None,
491
+ }
492
+ }
493
+
494
+ /// `run_watch`(`watch.py:25`)。`team-agent watch` 主循环:反复 `collect_watch_lines` + 输出 + sleep。
495
+ /// `output`/`sleep` 注入便于测试。§10 返 Result。
496
+ pub fn run_watch(
497
+ workspace: &WorkspacePath,
498
+ team: Option<&str>,
499
+ interval_sec: f64,
500
+ output: &mut dyn FnMut(&str),
501
+ ) -> Result<(), WatchError> {
502
+ let store = MessageStore::open(workspace.as_path())?;
503
+ let mut cursor = WatchCursor::default();
504
+ let interval = if interval_sec.is_finite() && interval_sec > 0.0 {
505
+ std::time::Duration::from_secs_f64(interval_sec)
506
+ } else {
507
+ std::time::Duration::from_millis(100)
508
+ };
509
+ loop {
510
+ for line in collect_watch_lines(workspace, &mut cursor, &store, team)? {
511
+ output(&line);
512
+ }
513
+ std::thread::sleep(interval);
514
+ }
515
+ }
516
+
517
+ /// watch 错误(读 events.jsonl / latest_results)。
518
+ #[derive(Debug, Error)]
519
+ pub enum WatchError {
520
+ #[error("io: {0}")]
521
+ Io(#[from] std::io::Error),
522
+ #[error("message store: {0}")]
523
+ MessageStore(#[from] crate::message_store::MessageStoreError),
524
+ }
525
+
526
+ fn file_signature(path: &Path) -> Result<Option<(u64, i128)>, WatchError> {
527
+ let meta = match std::fs::metadata(path) {
528
+ Ok(meta) => meta,
529
+ Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
530
+ Err(e) => return Err(WatchError::Io(e)),
531
+ };
532
+ let modified = meta.modified().ok();
533
+ let nanos = modified
534
+ .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
535
+ .and_then(|d| i128::try_from(d.as_nanos()).ok())
536
+ .unwrap_or(0);
537
+ Ok(Some((meta.len(), nanos)))
538
+ }
539
+
540
+ fn first_field<'a>(event: &'a Value, keys: &[&str]) -> Option<&'a str> {
541
+ keys.iter().find_map(|key| event.get(*key).and_then(Value::as_str))
542
+ }
543
+
544
+ fn clean_field(event: &Value, keys: &[&str], default: &str) -> String {
545
+ first_field(event, keys)
546
+ .map(clean_text)
547
+ .filter(|s| !s.is_empty())
548
+ .unwrap_or_else(|| default.to_string())
549
+ }
550
+
551
+ fn clean_text(text: &str) -> String {
552
+ text.split_whitespace().collect::<Vec<_>>().join(" ")
553
+ }
554
+
555
+ fn prefix_chars(text: &str, max: usize) -> String {
556
+ text.chars().take(max).collect()
557
+ }
@@ -0,0 +1,80 @@
1
+ //! step 12 · coordinator — daemon lifecycle / single-tick orchestration SKELETON (ROUND-0).
2
+ //!
3
+ //! Card: `docs/phase0/subsystems/12-coordinator.md`.
4
+ //! Truth source (READ-ONLY snapshot `team-agent-public` @ v0.2.11 / `439bef8`):
5
+ //! - `coordinator/__init__.py` (public re-export face; lazy `main` to break import cycle)
6
+ //! - `coordinator/__main__.py` (daemon main loop: pid/meta write, SIGTERM→STOP,
7
+ //! orphan self-detect, catch-all + exponential backoff 5→60s, tick_error dedupe/suppress,
8
+ //! tick_recovered)
9
+ //! - `coordinator/lifecycle.py` (health / start / stop / tick orchestration + schema health)
10
+ //! - `coordinator/metadata.py` (COORDINATOR_PROTOCOL_VERSION=2, pid_is_running, read/write/ok meta)
11
+ //! - `coordinator/paths.py` (coordinator.pid / coordinator.json / coordinator.log paths)
12
+ //! - `watch/__init__.py` (run_watch / collect_watch_lines / render_event_line / WatchCursor)
13
+ //! - `abnormal_track.py` (Gap 32 §4 provider-neutral abnormal track:
14
+ //! process_abnormal_records / detect_whole_team_gone)
15
+ //!
16
+ //! 职责(card §职责):per-workspace daemon 生命周期 + 单次 tick 编排。tick 按固定顺序把
17
+ //! step 8-11 的原子操作串成一个只读 + 投递既定 obligation 的回路 —— **绝不**在无 pending
18
+ //! obligation 时注入探索性 prompt(§10 MUST-NOT-13 / §84)。
19
+ //!
20
+ //! 铁律(card §bug/陷阱):
21
+ //! - **bug-084**:tick-end `save_runtime_state` 失败 → degraded `TickReport{ok:false,
22
+ //! reason:PersistenceDegraded, persisted:false}` + `runtime.state.save_failed` 事件,**绝不 panic**;
23
+ //! 主循环 catch + 指数退避 5→10→20→40→60→60s + 去重/抑制 tick_error。
24
+ //! - **bug-085 / unknown≠idle**:`TurnState::Unknown` 显式 block ping(穷尽 match,无 fallthrough);
25
+ //! `rollout_path` 用 `Option<RolloutPath>`;长期 unknown 第 60 tick 起每 12 tick
26
+ //! 发 `idle_takeover.unknown_persistent`。
27
+ //! - **take-over arm 来自真实投递**:监视器只能由真实 leader→worker 投递的 turn-open edge arm,
28
+ //! 绝不凭空 arm;无投递的队 → `not_armed_no_worker_turn`,绝不 ping。
29
+ //! - **schema 兼容门**:metadata 三元(pid/protocol_version/message_store_schema_version)任一不匹配
30
+ //! → restart_incompatible 先 stop 再起,**不可静默继续**用旧 schema 写库;区分 pre-init 必需列
31
+ //! (拒启)vs migratable 列(可迁移)。
32
+ //! - **孤儿不退 SIGTERM**:必须 SIGKILL 升级,优先按 pgid 杀整组。
33
+ //! - **孤儿自终止**:仅 `current_ppid != initial_ppid ∧ current_ppid == 1 ∧ workspace 不存在`
34
+ //! 三者同时成立才自杀。
35
+ //! - **§84 零注入**:无 pending obligation + event 时绝不注入探索性 prompt;compaction/drift/
36
+ //! api-error/idle 探测都是只读分类;唯一会投递的 `push_idle_reminder` 也只在 `should_ping` 时
37
+ //! 发一条中立 ack 提示。
38
+ //! - **整队消失区分 clean vs unexpected**:clean_shutdown/restart_in_progress 静默,
39
+ //! 仅 unexpected exit 写 durable marker + 延迟到下条 leader 命令再 escalate。
40
+ //! - **abnormal track 不读屏/不命名 provider**:只消费结构化 fault fact + 进程身份;
41
+ //! `(signature, turn_id)` 去重,turn_id 缺失退化为 per-record fingerprint 桶。
42
+ //! - **watch rotation**:archive_signature 变化或 offset>size 即插 ROTATION_MARKER 并重置 offset,
43
+ //! 不重放历史段。
44
+ //!
45
+ //! ROUND-0:仅类型 + struct/enum + fn/trait/method 签名。所有 body =
46
+ //! `unimplemented!("step12 port: <what>")`。fallible 路径返 `Result`/`Option`(§10 实现层禁
47
+ //! unwrap/expect/panic,签名先做成 fallible)。daemon-path `tick(..) -> Result<TickReport, TickError>`。
48
+ //! `#![deny(...)]` 由 leader 在集成时统一加,本骨架不加。
49
+
50
+ // ROUND-0 skeleton:fn body 全 unimplemented!() → import/field/method 暂未被用;P2 porter 落实现时移除。
51
+ #![allow(dead_code, unused_imports)]
52
+ // §10:daemon-path(tick/abnormal/health)实现层禁 unwrap/expect/panic(unimplemented!() stub 不被拦);
53
+ // tests 子模块各自 allow。bug-084:tick-end persist 失败走 degraded TickReport,绝不 panic。
54
+ #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
55
+
56
+ // ── module-root REUSE imports ─────────────────────────────────────────────
57
+ // decompose 前这些 `use` 在 coordinator.rs 顶层,测试 `mod tests` 经 `use super::*`
58
+ // 继承它们。拆分后保留在 mod.rs(私有 use,子模块 tests 仍经 `use super::*` 解析)。
59
+ use crate::message_store::MessageStore;
60
+ use crate::model::enums::Provider;
61
+ use crate::provider::{ProviderAdapter, TurnId, TurnState};
62
+ use serde_json::Value;
63
+
64
+ pub mod backoff;
65
+ pub mod health;
66
+ pub mod orphan;
67
+ pub mod tick;
68
+ pub mod types;
69
+
70
+ // ── Re-export the full module-root surface (RE-EXPORT INVARIANT) ──────────────
71
+ // 这些 `pub use`/`pub(crate) use` 把每个曾在 module 根可见的 item 重新 surface 到
72
+ // `crate::coordinator::X`,使外部 crate 路径 + 测试 `use super::*` 解析保持不变。
73
+ pub use types::*;
74
+ pub use tick::*;
75
+ pub use backoff::*;
76
+ pub use orphan::*;
77
+ pub use health::*;
78
+
79
+ #[cfg(test)]
80
+ mod tests;