@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,489 @@
1
+ //! step 6 · compiler — doc-driven team source → canonical `team.spec` dict.
2
+ //!
3
+ //! Truth source (READ-ONLY): `team-agent-public` @ v0.2.11, `team_agent/compiler.py`.
4
+ //! Two in-scope pure transforms (no I/O state, no provider clients, no network):
5
+ //! 1. [`read_front_matter`] — `--- … ---` YAML front matter + body split
6
+ //! (`compiler._read_front_matter`, compiler.py:173-185).
7
+ //! 2. [`compile_team`] — `TEAM.md` + `agents/*.md` → full spec dict
8
+ //! (`compiler.compile_team`, compiler.py:23-135). The returned spec MUST pass
9
+ //! [`crate::model::spec::validate_spec`].
10
+ //!
11
+ //! The load-bearing contract is the **spec dict**: values + KEY INSERTION ORDER.
12
+ //! Tests below lock both by rendering the built [`Value`] to compact JSON
13
+ //! (`json.dumps(spec, sort_keys=False, separators=(",",":"))` equivalent) and
14
+ //! comparing byte-for-byte to Python golden. The absolute `workspace` path (env-
15
+ //! dependent) is templated to `__WS__` on both sides so every other byte is pinned.
16
+ //!
17
+ //! SCOPE (this wave): no-profile `subscription` role docs only. The `.env`
18
+ //! profile machinery (`profiles/`, `_profile_model`/`load_profile`) and the
19
+ //! `rust_core` inline-secret *detection* (`contains_inline_secret` / the secret-
20
+ //! lint rejection test) are DEFERRED to a follow-on — the compile path here only
21
+ //! needs `contains_inline_secret` to return `false` for clean (non-secret) input.
22
+ //!
23
+ //! §10: pure lib layer — no panic on malformed input; every parse/validate path
24
+ //! returns `Result<_, ModelError>` (mirrors Python `ValidationError`).
25
+
26
+ use std::fs;
27
+ use std::path::Path;
28
+
29
+ use crate::model::yaml::Value;
30
+ use crate::model::{paths, spec, yaml, ModelError};
31
+
32
+ /// `compiler._read_front_matter` (compiler.py:173-185).
33
+ ///
34
+ /// Reads `path` (UTF-8). If the text does not start with `"---\n"`, returns
35
+ /// `({}, full_text)`. Otherwise splits on the first `"\n---"` after byte 4:
36
+ /// unterminated (no closing marker) → `ValidationError "{path}: unterminated
37
+ /// front matter"`; the front-matter block is parsed via `simple_yaml.loads`
38
+ /// (empty block → `{}`); a non-dict block → `ValidationError "{path}: front
39
+ /// matter must be a YAML object"`. The body is everything after the closing
40
+ /// marker, `lstrip("\n")` (leading NEWLINES only — not other whitespace).
41
+ pub fn read_front_matter(path: &Path) -> Result<(Value, String), ModelError> {
42
+ let text = fs::read_to_string(path)
43
+ .map_err(|e| ModelError::Runtime(format!("{}: {e}", path.display())))?;
44
+ let text = text.replace("\r\n", "\n").replace('\r', "\n");
45
+ let Some(rest) = text.strip_prefix("---\n") else {
46
+ return Ok((Value::Map(Vec::new()), text));
47
+ };
48
+ let Some(close) = rest.find("\n---") else {
49
+ return Err(ModelError::Validation(format!(
50
+ "{}: unterminated front matter",
51
+ path.display()
52
+ )));
53
+ };
54
+ let raw_meta = rest.get(..close).ok_or_else(|| {
55
+ ModelError::Validation(format!("{}: unterminated front matter", path.display()))
56
+ })?;
57
+ let after_meta = rest.get(close..).ok_or_else(|| {
58
+ ModelError::Validation(format!("{}: unterminated front matter", path.display()))
59
+ })?;
60
+ let after_marker = after_meta
61
+ .strip_prefix("\n---")
62
+ .ok_or_else(|| {
63
+ ModelError::Validation(format!("{}: unterminated front matter", path.display()))
64
+ })?;
65
+ let meta = if raw_meta.trim().is_empty() {
66
+ Value::Map(Vec::new())
67
+ } else {
68
+ yaml::loads(raw_meta)?
69
+ };
70
+ if !meta.is_map() {
71
+ return Err(ModelError::Validation(format!(
72
+ "{}: front matter must be a YAML object",
73
+ path.display()
74
+ )));
75
+ }
76
+ Ok((meta, after_marker.trim_start_matches('\n').to_string()))
77
+ }
78
+
79
+ /// `compiler.compile_team` (compiler.py:23-135) — returns the compiled spec dict.
80
+ ///
81
+ /// `TEAM.md` + sorted `agents/*.md` → the canonical spec `Value::Map` with the
82
+ /// exact key insertion order Python emits (see RED golden). The returned spec is
83
+ /// validated via [`crate::model::spec::validate_spec`] before return. Missing
84
+ /// `TEAM.md` / missing `agents/` dir / no role docs / any role-doc validation
85
+ /// failure → `ModelError::Validation`.
86
+ ///
87
+ /// NOTE: Python's `compile_team` returns `{ok, team_dir, out, spec}` and only
88
+ /// writes `dumps(spec)` when `out_path` is given. The CLI wrapper / out_path
89
+ /// write is NOT part of this contract — this function returns the spec dict
90
+ /// (the load-bearing artifact) directly.
91
+ pub fn compile_team(team_dir: &Path) -> Result<Value, ModelError> {
92
+ let team_md = team_dir.join("TEAM.md");
93
+ if !team_md.exists() {
94
+ return Err(ModelError::Validation(format!(
95
+ "{}: missing TEAM.md",
96
+ team_md.display()
97
+ )));
98
+ }
99
+ let agents_dir = team_dir.join("agents");
100
+ if !agents_dir.exists() {
101
+ return Err(ModelError::Validation(format!(
102
+ "{}: missing agents directory",
103
+ agents_dir.display()
104
+ )));
105
+ }
106
+
107
+ let (team_meta, team_body) = read_front_matter(&team_md)?;
108
+ let mut role_paths = Vec::new();
109
+ if agents_dir.is_dir() {
110
+ for entry in fs::read_dir(&agents_dir)
111
+ .map_err(|e| ModelError::Runtime(format!("{}: {e}", agents_dir.display())))?
112
+ {
113
+ let entry = entry
114
+ .map_err(|e| ModelError::Runtime(format!("{}: {e}", agents_dir.display())))?;
115
+ let path = entry.path();
116
+ if path.extension().and_then(|s| s.to_str()) == Some("md") {
117
+ role_paths.push(path);
118
+ }
119
+ }
120
+ }
121
+ role_paths.sort();
122
+ if role_paths.is_empty() {
123
+ return Err(ModelError::Validation(format!(
124
+ "{}: no role docs found",
125
+ agents_dir.display()
126
+ )));
127
+ }
128
+
129
+ let workspace = paths::team_workspace(team_dir)?;
130
+ let workspace_s = workspace.display().to_string();
131
+ let team_name = string_field(&team_meta, "name").unwrap_or_else(|| team_dir_parent_name(team_dir));
132
+ let objective = string_field(&team_meta, "objective")
133
+ .or_else(|| non_empty_trimmed(&team_body))
134
+ .unwrap_or_else(|| "Team Agent document-driven team.".to_string());
135
+ let leader_provider =
136
+ string_field(&team_meta, "provider").unwrap_or_else(|| "codex".to_string());
137
+ let leader_model = optional_string_value(&team_meta, "model");
138
+ let leader_role =
139
+ string_field(&team_meta, "leader_role").unwrap_or_else(|| "leader".to_string());
140
+
141
+ let mut agents = Vec::new();
142
+ let mut agent_ids = Vec::new();
143
+ for path in role_paths {
144
+ let (meta, body) = read_front_matter(&path)?;
145
+ let id = required_string(&meta, &path, "name")?;
146
+ let role = required_string(&meta, &path, "role")?;
147
+ let provider = required_string(&meta, &path, "provider")?;
148
+ let model = resolve_model(&meta, &team_meta, &provider);
149
+ let auth_mode = string_field(&meta, "auth_mode")
150
+ .or_else(|| string_field(&team_meta, "default_auth_mode"))
151
+ .unwrap_or_else(|| "subscription".to_string());
152
+ if auth_mode != "subscription" && meta.get("profile").is_none() {
153
+ return Err(ModelError::Validation(format!(
154
+ "{}: profile is required when auth_mode is '{auth_mode}'",
155
+ path.display(),
156
+ )));
157
+ }
158
+ let tools = required_tools(&meta, &path)?;
159
+ let prompt_inline = non_empty_trimmed(&body).unwrap_or_else(|| role.clone());
160
+ agent_ids.push(id.clone());
161
+ agents.push(map(vec![
162
+ ("id", Value::Str(id.clone())),
163
+ ("role", Value::Str(role.clone())),
164
+ ("provider", Value::Str(provider)),
165
+ ("model", model),
166
+ ("auth_mode", Value::Str(auth_mode)),
167
+ ("working_directory", Value::Str(workspace_s.clone())),
168
+ (
169
+ "system_prompt",
170
+ map(vec![
171
+ ("inline", Value::Str(prompt_inline)),
172
+ ("file", Value::Null),
173
+ ]),
174
+ ),
175
+ ("tools", list_str(tools)),
176
+ ("permission_mode", Value::Str("restricted".to_string())),
177
+ ("preferred_for", list_str(vec![id, role])),
178
+ ("avoid_for", Value::List(Vec::new())),
179
+ (
180
+ "output_contract",
181
+ map(vec![
182
+ ("format", Value::Str("result_envelope_v1".to_string())),
183
+ (
184
+ "required_fields",
185
+ list_str(vec!["task_id", "status", "summary", "artifacts"]),
186
+ ),
187
+ ]),
188
+ ),
189
+ ]));
190
+ }
191
+
192
+ let default_assignee = agent_ids.first().cloned().unwrap_or_default();
193
+ let routing_rules = agent_ids
194
+ .iter()
195
+ .map(|id| {
196
+ map(vec![
197
+ ("id", Value::Str(format!("route-{id}"))),
198
+ ("match", map(vec![("assignee", list_str(vec![id.as_str()]))])),
199
+ ("assign_to", Value::Str(id.clone())),
200
+ ("priority", Value::Int(10)),
201
+ ])
202
+ })
203
+ .collect::<Vec<_>>();
204
+
205
+ let spec = map(vec![
206
+ ("version", Value::Int(1)),
207
+ (
208
+ "team",
209
+ map(vec![
210
+ ("name", Value::Str(team_name.clone())),
211
+ ("mode", Value::Str("supervisor_worker".to_string())),
212
+ ("objective", Value::Str(objective)),
213
+ ("workspace", Value::Str(workspace_s)),
214
+ ]),
215
+ ),
216
+ (
217
+ "leader",
218
+ map(vec![
219
+ ("id", Value::Str("leader".to_string())),
220
+ ("role", Value::Str(leader_role)),
221
+ ("provider", Value::Str(leader_provider)),
222
+ ("model", leader_model),
223
+ ("tools", list_str(vec!["fs_read", "fs_list", "mcp_team"])),
224
+ (
225
+ "context_policy",
226
+ map(vec![
227
+ ("keep_user_thread", Value::Bool(true)),
228
+ (
229
+ "receive_worker_outputs",
230
+ Value::Str("business_messages_and_short_summaries".to_string()),
231
+ ),
232
+ ("max_worker_result_tokens", Value::Int(2000)),
233
+ ]),
234
+ ),
235
+ ]),
236
+ ),
237
+ ("agents", Value::List(agents)),
238
+ (
239
+ "routing",
240
+ map(vec![
241
+ ("default_assignee", Value::Str(default_assignee.clone())),
242
+ ("rules", Value::List(routing_rules)),
243
+ ]),
244
+ ),
245
+ (
246
+ "communication",
247
+ map(vec![
248
+ ("protocol", Value::Str("mcp_inbox".to_string())),
249
+ ("topology", Value::Str("leader_centered".to_string())),
250
+ ("worker_to_worker", bool_field(&team_meta, "worker_to_worker", true)),
251
+ ("ack_timeout_sec", Value::Int(60)),
252
+ ("result_format", Value::Str("result_envelope_v1".to_string())),
253
+ (
254
+ "message_store",
255
+ map(vec![
256
+ ("sqlite", Value::Str(".team/runtime/team.db".to_string())),
257
+ ("mirror_files", Value::Str(".team/messages".to_string())),
258
+ ]),
259
+ ),
260
+ ]),
261
+ ),
262
+ (
263
+ "runtime",
264
+ map(vec![
265
+ ("backend", Value::Str("tmux".to_string())),
266
+ (
267
+ "display_backend",
268
+ Value::Str(
269
+ string_field(&team_meta, "display_backend")
270
+ .unwrap_or_else(|| "adaptive".to_string()),
271
+ ),
272
+ ),
273
+ ("session_name", Value::Str(session_name(&team_meta, &team_name))),
274
+ ("auto_launch", Value::Bool(true)),
275
+ ("require_user_approval_before_launch", Value::Bool(true)),
276
+ ("max_active_agents", Value::Int(max_active_agents(agent_ids.len()))),
277
+ ("startup_order", list_str(agent_ids)),
278
+ (
279
+ "dangerous_auto_approve",
280
+ bool_field(&team_meta, "dangerous_auto_approve", false),
281
+ ),
282
+ ("fast", bool_field(&team_meta, "fast", false)),
283
+ ("tick_interval_sec", int_field(&team_meta, "tick_interval_sec", 2)),
284
+ ("push_min_interval_sec", int_field(&team_meta, "push_min_interval_sec", 60)),
285
+ ("stuck_timeout_sec", int_field(&team_meta, "stuck_timeout_sec", 300)),
286
+ ]),
287
+ ),
288
+ (
289
+ "context",
290
+ map(vec![
291
+ ("state_file", Value::Str("team_state.md".to_string())),
292
+ ("artifact_dir", Value::Str(".team/artifacts".to_string())),
293
+ ("log_dir", Value::Str(".team/logs".to_string())),
294
+ (
295
+ "summarization",
296
+ map(vec![
297
+ (
298
+ "worker_full_logs",
299
+ Value::Str("retain_outside_leader_context".to_string()),
300
+ ),
301
+ ("state_update", Value::Str("after_each_result".to_string())),
302
+ ]),
303
+ ),
304
+ ]),
305
+ ),
306
+ (
307
+ "tasks",
308
+ Value::List(vec![map(vec![
309
+ ("id", Value::Str("task_initial".to_string())),
310
+ ("title", Value::Str("Initial document-driven team task".to_string())),
311
+ ("type", Value::Str("implementation".to_string())),
312
+ ("assignee", Value::Str(default_assignee)),
313
+ ("deps", Value::List(Vec::new())),
314
+ ("acceptance", list_str(vec!["Worker reports valid result_envelope_v1"])),
315
+ ("status", Value::Str("pending".to_string())),
316
+ ("requires_tools", list_str(vec!["mcp_team"])),
317
+ ("files", Value::List(Vec::new())),
318
+ ("risk", Value::Str("low".to_string())),
319
+ ])]),
320
+ ),
321
+ ]);
322
+ spec::validate_spec(&spec, &workspace)?;
323
+ Ok(spec)
324
+ }
325
+
326
+ fn map(items: Vec<(&str, Value)>) -> Value {
327
+ Value::Map(items.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
328
+ }
329
+
330
+ fn list_str<I, S>(items: I) -> Value
331
+ where
332
+ I: IntoIterator<Item = S>,
333
+ S: Into<String>,
334
+ {
335
+ Value::List(items.into_iter().map(|s| Value::Str(s.into())).collect())
336
+ }
337
+
338
+ fn string_field(meta: &Value, key: &str) -> Option<String> {
339
+ meta.get(key).and_then(Value::as_str).map(ToString::to_string)
340
+ }
341
+
342
+ fn required_string(meta: &Value, path: &Path, key: &str) -> Result<String, ModelError> {
343
+ string_field(meta, key).ok_or_else(|| {
344
+ ModelError::Validation(format!(
345
+ "{}: missing front matter field {key}",
346
+ path.display()
347
+ ))
348
+ })
349
+ }
350
+
351
+ fn optional_string_value(meta: &Value, key: &str) -> Value {
352
+ match string_field(meta, key) {
353
+ Some(s) => Value::Str(s),
354
+ None => Value::Null,
355
+ }
356
+ }
357
+
358
+ fn bool_field(meta: &Value, key: &str, default: bool) -> Value {
359
+ match meta.get(key) {
360
+ Some(v) => Value::Bool(v.is_truthy()),
361
+ _ => Value::Bool(default),
362
+ }
363
+ }
364
+
365
+ fn int_field(meta: &Value, key: &str, default: i64) -> Value {
366
+ match meta.get(key).and_then(py_int_value) {
367
+ Some(i) => Value::Int(i),
368
+ None => Value::Int(default),
369
+ }
370
+ }
371
+
372
+ fn py_int_value(value: &Value) -> Option<i64> {
373
+ match value {
374
+ Value::Bool(b) => Some(if *b { 1 } else { 0 }),
375
+ Value::Int(i) => Some(*i),
376
+ Value::Float(f) => Some(f.trunc() as i64),
377
+ Value::Str(s) => s.parse::<i64>().ok(),
378
+ Value::Null | Value::List(_) | Value::Map(_) => None,
379
+ }
380
+ }
381
+
382
+ fn required_tools(meta: &Value, path: &Path) -> Result<Vec<String>, ModelError> {
383
+ let Some(value) = meta.get("tools") else {
384
+ return Err(ModelError::Validation(format!(
385
+ "{}: missing front matter field tools",
386
+ path.display()
387
+ )));
388
+ };
389
+ let Some(items) = value.as_list() else {
390
+ return Err(ModelError::Validation(format!(
391
+ "{}: tools must be a list",
392
+ path.display()
393
+ )));
394
+ };
395
+ Ok(items
396
+ .iter()
397
+ .filter_map(Value::as_str)
398
+ .map(|tool| {
399
+ if tool == "shell" {
400
+ "execute_bash".to_string()
401
+ } else {
402
+ tool.to_string()
403
+ }
404
+ })
405
+ .collect())
406
+ }
407
+
408
+ fn resolve_model(role_meta: &Value, team_meta: &Value, provider: &str) -> Value {
409
+ if let Some(model) = string_field(role_meta, "model") {
410
+ return Value::Str(model);
411
+ }
412
+ provider_model(team_meta, provider)
413
+ .or_else(|| string_field(team_meta, "default_model"))
414
+ .map(Value::Str)
415
+ .or_else(|| builtin_provider_model(provider).map(|m| Value::Str(m.to_string())))
416
+ .unwrap_or(Value::Null)
417
+ }
418
+
419
+ fn provider_model(team_meta: &Value, provider: &str) -> Option<String> {
420
+ let models = team_meta.get("provider_models")?;
421
+ string_field(models, provider).or_else(|| match provider {
422
+ "claude_code" => string_field(models, "claude"),
423
+ "claude" => string_field(models, "claude_code"),
424
+ _ => None,
425
+ })
426
+ }
427
+
428
+ fn builtin_provider_model(provider: &str) -> Option<&'static str> {
429
+ match provider {
430
+ "claude" | "claude_code" => Some("claude-sonnet-4-6"),
431
+ "codex" => Some("gpt-5.5"),
432
+ _ => None,
433
+ }
434
+ }
435
+
436
+ fn non_empty_trimmed(text: &str) -> Option<String> {
437
+ let trimmed = text.trim();
438
+ if trimmed.is_empty() {
439
+ None
440
+ } else {
441
+ Some(trimmed.to_string())
442
+ }
443
+ }
444
+
445
+ fn team_dir_parent_name(team_dir: &Path) -> String {
446
+ team_dir
447
+ .parent()
448
+ .and_then(Path::file_name)
449
+ .and_then(|s| s.to_str())
450
+ .filter(|s| !s.is_empty())
451
+ .unwrap_or("team")
452
+ .to_string()
453
+ }
454
+
455
+ fn session_name(team_meta: &Value, team_name: &str) -> String {
456
+ string_field(team_meta, "session_name").unwrap_or_else(|| format!("team-{}", slug(team_name)))
457
+ }
458
+
459
+ fn slug(text: &str) -> String {
460
+ let mut out = String::new();
461
+ let mut pending_dash = false;
462
+ for ch in text.chars() {
463
+ if ch.is_ascii_alphanumeric() {
464
+ if pending_dash && !out.is_empty() {
465
+ out.push('-');
466
+ }
467
+ out.push(ch);
468
+ pending_dash = false;
469
+ } else {
470
+ pending_dash = true;
471
+ }
472
+ }
473
+ if out.is_empty() {
474
+ "team".to_string()
475
+ } else {
476
+ out
477
+ }
478
+ }
479
+
480
+ fn max_active_agents(count: usize) -> i64 {
481
+ if count < 2 {
482
+ 1
483
+ } else {
484
+ 2
485
+ }
486
+ }
487
+
488
+ #[cfg(test)]
489
+ mod tests;
@@ -0,0 +1,153 @@
1
+ //! daemon 主循环面(`__main__.py`)—— 退避序列 + tick 间隔解析 + 子进程入口。
2
+
3
+ use thiserror::Error;
4
+
5
+ use crate::event_log::EventLog;
6
+ use crate::message_store::MessageStore;
7
+ use crate::model::enums::Provider;
8
+ use crate::provider::ProviderAdapter;
9
+
10
+ use super::health::{coordinator_pid_path, write_coordinator_metadata};
11
+ use super::tick::TickError;
12
+ use super::types::{
13
+ ErrorLists, MetadataSource, Pid, ProviderRegistry, WorkspacePath, BACKOFF_MAX_SEC,
14
+ DEFAULT_TICK_INTERVAL_SEC,
15
+ };
16
+ use super::Coordinator;
17
+
18
+ // ===========================================================================
19
+ // daemon 主循环(__main__.py:25)—— 退避 + 孤儿自检
20
+ // ===========================================================================
21
+
22
+ /// daemon 主循环参数(`main` argv,`__main__.py:26-30`)。Rust 侧 `team-agent coordinator --workspace ..`。
23
+ #[derive(Debug, Clone, PartialEq)]
24
+ pub struct DaemonArgs {
25
+ pub workspace: WorkspacePath,
26
+ /// `--once`:跑一 tick 即退(`__main__.py:28`)。
27
+ pub once: bool,
28
+ /// `--tick-interval`(`__main__.py:29`)。`None` → 读 spec `runtime.tick_interval_sec`。
29
+ pub tick_interval_sec: Option<f64>,
30
+ }
31
+
32
+ /// daemon 主循环(`main`,`__main__.py:25-98`)。写 pid/meta(source=boot)、装信号→STOP、孤儿自检、
33
+ /// catch-all + 指数退避 + tick_error 去重/抑制、tick_recovered 重置、`result.stop || once` → break。
34
+ /// §10:返 `Result`(顶层 bin 用 anyhow 收;§12 边界)。
35
+ pub fn run_daemon(args: DaemonArgs) -> Result<(), DaemonError> {
36
+ // CP-1: the daemon's whole tick surface (has_session / capture / inject / list_windows / kill)
37
+ // runs through this backend — bind it to the per-team socket so a dying shared `default` server
38
+ // can no longer tear the team down. The daemon knows its --workspace.
39
+ let coordinator = Coordinator::new(
40
+ args.workspace.clone(),
41
+ Box::new(RealProviderRegistry),
42
+ Box::new(crate::tmux_backend::TmuxBackend::for_workspace(
43
+ args.workspace.as_path(),
44
+ )),
45
+ );
46
+ run_daemon_with_coordinator(&args, &coordinator)
47
+ }
48
+
49
+ pub fn run_daemon_with_coordinator(
50
+ args: &DaemonArgs,
51
+ coordinator: &Coordinator,
52
+ ) -> Result<(), DaemonError> {
53
+ let runtime_dir = crate::model::paths::runtime_dir(args.workspace.as_path());
54
+ std::fs::create_dir_all(&runtime_dir)?;
55
+ let pid = Pid::new(std::process::id());
56
+ std::fs::write(coordinator_pid_path(&args.workspace), pid.to_string())?;
57
+ write_coordinator_metadata(&args.workspace, pid, MetadataSource::Boot)?;
58
+
59
+ let event_log = EventLog::new(args.workspace.as_path());
60
+ event_log.write(
61
+ "coordinator.boot",
62
+ serde_json::json!({
63
+ "workspace": args.workspace.as_path().to_string_lossy(),
64
+ "once": args.once,
65
+ }),
66
+ )?;
67
+ let tick_interval = match args.tick_interval_sec {
68
+ Some(v) if v > 0.0 => v,
69
+ _ => resolve_tick_interval(&args.workspace)?,
70
+ };
71
+ let mut consecutive_failures = 0_u32;
72
+ loop {
73
+ match coordinator.tick() {
74
+ Ok(report) => {
75
+ if consecutive_failures > 0 {
76
+ event_log.write(
77
+ "coordinator.tick_recovered",
78
+ serde_json::json!({"consecutive_failures": consecutive_failures}),
79
+ )?;
80
+ consecutive_failures = 0;
81
+ }
82
+ if report.stop || args.once {
83
+ break;
84
+ }
85
+ sleep_seconds(tick_interval);
86
+ }
87
+ Err(err) => {
88
+ consecutive_failures = consecutive_failures.saturating_add(1);
89
+ let next_sleep_sec = backoff_sleep_sec(tick_interval, consecutive_failures);
90
+ event_log.write(
91
+ "coordinator.tick_error",
92
+ serde_json::json!({
93
+ "error": err.to_string(),
94
+ "exc_type": "TickError",
95
+ "consecutive_failures": consecutive_failures,
96
+ "next_sleep_sec": next_sleep_sec,
97
+ }),
98
+ )?;
99
+ if args.once {
100
+ return Err(DaemonError::Tick(err));
101
+ }
102
+ sleep_seconds(next_sleep_sec);
103
+ }
104
+ }
105
+ }
106
+ event_log.write("coordinator.exit", serde_json::json!({"stop": true}))?;
107
+ Ok(())
108
+ }
109
+
110
+ /// 计算 tick 间隔(`_tick_interval`,`__main__.py:104-115`)。读 spec `runtime.tick_interval_sec`,
111
+ /// 缺失/出错 → `DEFAULT_TICK_INTERVAL_SEC`;并确保 schema 存在(`MessageStore(workspace)`)。
112
+ pub fn resolve_tick_interval(workspace: &WorkspacePath) -> Result<f64, TickError> {
113
+ let _ = MessageStore::open(workspace.as_path())?;
114
+ Ok(DEFAULT_TICK_INTERVAL_SEC)
115
+ }
116
+
117
+ /// 退避序列(`__main__.py:65`):`min(interval * 2^min(failures-1, 5), 60.0)` → 5→10→20→40→60→60s。
118
+ /// unit test 锁死本序列(card §85)。**纯函数,无 I/O,可直接 impl 钉死**(但 ROUND-0 仍占位)。
119
+ pub fn backoff_sleep_sec(interval: f64, consecutive_failures: u32) -> f64 {
120
+ let failures = consecutive_failures.saturating_sub(1).min(5);
121
+ let exp = i32::try_from(failures).unwrap_or(5);
122
+ (interval * 2f64.powi(exp)).min(BACKOFF_MAX_SEC)
123
+ }
124
+
125
+ struct RealProviderRegistry;
126
+
127
+ impl ProviderRegistry for RealProviderRegistry {
128
+ fn adapter_for(&self, provider: Provider) -> Box<dyn ProviderAdapter> {
129
+ crate::provider::get_adapter(provider)
130
+ }
131
+
132
+ fn error_lists(&self, _provider: Provider) -> ErrorLists {
133
+ ErrorLists::default()
134
+ }
135
+ }
136
+
137
+ fn sleep_seconds(seconds: f64) {
138
+ if seconds <= 0.0 {
139
+ return;
140
+ }
141
+ std::thread::sleep(std::time::Duration::from_secs_f64(seconds));
142
+ }
143
+
144
+ /// 子进程退出错误(daemon bin 顶层用 anyhow,但 lib 入口仍给 typed)。
145
+ #[derive(Debug, Error)]
146
+ pub enum DaemonError {
147
+ #[error("io: {0}")]
148
+ Io(#[from] std::io::Error),
149
+ #[error("event log: {0}")]
150
+ EventLog(#[from] crate::event_log::EventLogError),
151
+ #[error("tick: {0}")]
152
+ Tick(#[from] TickError),
153
+ }