@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
@@ -1,693 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import hashlib
4
- import errno
5
- import json
6
- import os
7
- import copy
8
- import subprocess
9
- import time
10
- import uuid
11
- from datetime import datetime, timezone
12
- from pathlib import Path
13
- from typing import Any
14
-
15
- from team_agent.paths import runtime_dir
16
- from team_agent.simple_yaml import dumps
17
-
18
-
19
- SESSION_CAPTURE_FIELDS = [
20
- "session_id",
21
- "rollout_path",
22
- "captured_at",
23
- "captured_via",
24
- "attribution_confidence",
25
- ]
26
- SESSION_STATE_FIELDS = [
27
- *SESSION_CAPTURE_FIELDS,
28
- "spawn_cwd",
29
- ]
30
- _UUID_SEPARATOR = "\0"
31
- _RUNTIME_STATE_CACHE: dict[str, dict[str, Any]] = {}
32
-
33
-
34
- def derive_leader_session_uuid(machine_fingerprint: str, workspace_abspath: str, os_user: str, team_id: str) -> str:
35
- parts = [machine_fingerprint, workspace_abspath, os_user, team_id]
36
- if any(_UUID_SEPARATOR in part for part in parts):
37
- raise ValueError("leader_session_uuid inputs must not contain NUL")
38
- return hashlib.sha256(_UUID_SEPARATOR.join(parts).encode("utf-8")).hexdigest()[:32]
39
-
40
-
41
- def runtime_state_path(workspace: Path) -> Path:
42
- return runtime_dir(workspace) / "state.json"
43
-
44
-
45
- def normalize_agent_session_state(state: dict[str, Any]) -> None:
46
- agents = state.get("agents", {})
47
- if not isinstance(agents, dict):
48
- return
49
- for agent_state in agents.values():
50
- if isinstance(agent_state, dict):
51
- for field in SESSION_STATE_FIELDS:
52
- agent_state.setdefault(field, None)
53
-
54
-
55
- def load_runtime_state(workspace: Path) -> dict[str, Any]:
56
- path = runtime_state_path(workspace)
57
- if not path.exists():
58
- cached = _RUNTIME_STATE_CACHE.get(str(path))
59
- if cached is not None:
60
- return copy.deepcopy(cached)
61
- return {"agents": {}, "tasks": [], "session_name": None, "active_team_key": None}
62
- state = json.loads(path.read_text(encoding="utf-8"))
63
- normalize_agent_session_state(state)
64
- changed = _migrate_state_identity(state, workspace)
65
- if _migrate_active_team_key(state):
66
- changed = True
67
- if changed:
68
- save_runtime_state(workspace, state)
69
- _RUNTIME_STATE_CACHE[str(path)] = copy.deepcopy(state)
70
- return state
71
-
72
-
73
- def _migrate_active_team_key(state: dict[str, Any]) -> bool:
74
- """0.2.6 Family B (C6): legacy states with a top-level ``session_name``
75
- but no ``active_team_key`` get the active pointer seeded once. After
76
- this, ``active_team_key`` is the single explicit source of truth and
77
- callers mutate it through CLI verbs (claim-leader / takeover /
78
- shutdown / restart)."""
79
- if "active_team_key" in state:
80
- return False
81
- teams = state.get("teams") if isinstance(state.get("teams"), dict) else {}
82
- if state.get("session_name"):
83
- seed = team_state_key(state)
84
- state["active_team_key"] = seed if seed in teams or not teams else seed
85
- return True
86
- if isinstance(teams, dict) and len(teams) == 1:
87
- state["active_team_key"] = next(iter(teams))
88
- return True
89
- state["active_team_key"] = None
90
- return True
91
-
92
-
93
- def team_state_key(state: dict[str, Any]) -> str:
94
- for field in ("team_dir", "spec_path"):
95
- value = state.get(field)
96
- if not value:
97
- continue
98
- path = Path(str(value))
99
- key = path.name if field == "team_dir" else path.parent.name
100
- if key and key not in {".team", "runtime"}:
101
- return key
102
- return str(state.get("session_name") or "current")
103
-
104
-
105
- def compact_team_state(state: dict[str, Any]) -> dict[str, Any]:
106
- compact = copy.deepcopy(state)
107
- compact.pop("teams", None)
108
- return compact
109
-
110
-
111
- def merge_workspace_team_state(existing: dict[str, Any], launched: dict[str, Any]) -> dict[str, Any]:
112
- launched_key = team_state_key(launched)
113
- if not existing.get("session_name"):
114
- merged = copy.deepcopy(launched)
115
- merged.setdefault("teams", {})[launched_key] = compact_team_state(launched)
116
- return merged
117
- existing_key = team_state_key(existing)
118
- if existing_key == launched_key:
119
- merged = copy.deepcopy(launched)
120
- teams = copy.deepcopy(existing.get("teams") or {})
121
- teams[launched_key] = compact_team_state(launched)
122
- merged["teams"] = teams
123
- return merged
124
- merged = copy.deepcopy(existing)
125
- teams = merged.setdefault("teams", {})
126
- teams.setdefault(existing_key, compact_team_state(existing))
127
- teams[launched_key] = compact_team_state(launched)
128
- return merged
129
-
130
-
131
- def team_state_candidates(state: dict[str, Any]) -> dict[str, dict[str, Any]]:
132
- """0.2.6 Family B (C7): the only candidate source is ``state.teams``
133
- filtered by ``status == "alive"``. Top-level ``session_name`` /
134
- ``team_dir`` are a derived view of the active team and never count as
135
- an independent candidate. Shutdown/legacy entries with non-alive
136
- status are excluded."""
137
- out: dict[str, dict[str, Any]] = {}
138
- teams = state.get("teams") if isinstance(state.get("teams"), dict) else {}
139
- for key, value in teams.items():
140
- if not isinstance(value, dict):
141
- continue
142
- if str(value.get("status") or "alive").lower() != "alive":
143
- continue
144
- out[str(key)] = value
145
- return out
146
-
147
-
148
- def format_team_candidates(team_states: dict[str, dict[str, Any]]) -> str:
149
- if not team_states:
150
- return "No team state was found."
151
- parts = []
152
- for key in sorted(team_states):
153
- st = team_states[key]
154
- agents = ",".join(sorted(st.get("agents", {}).keys())) or "-"
155
- parts.append(f"{key} session={st.get('session_name') or '-'} agents={agents}")
156
- return "Candidates: " + "; ".join(parts)
157
-
158
-
159
- def _team_entry_from_state(state: dict[str, Any], team_key: str) -> dict[str, Any] | None:
160
- teams = state.get("teams") if isinstance(state.get("teams"), dict) else {}
161
- entry = teams.get(team_key)
162
- if not isinstance(entry, dict):
163
- return None
164
- return entry
165
-
166
-
167
- def _project_top_level_view(state: dict[str, Any], team_key: str) -> dict[str, Any]:
168
- """0.2.6 Family B (C8): when picking a team for use, the top-level
169
- keys (``session_name`` / ``team_dir`` / ``agents`` / ``tasks``) are a
170
- derived view of ``teams[team_key]``. We copy the team entry into a
171
- flat dict and preserve any auxiliary state (``team_owner`` /
172
- ``leader_receiver`` / ``coordinator`` already pinned to the team)."""
173
- entry = _team_entry_from_state(state, team_key) or {}
174
- projection = copy.deepcopy(entry)
175
- projection.setdefault("session_name", entry.get("session_name"))
176
- projection.setdefault("team_dir", entry.get("team_dir"))
177
- projection["active_team_key"] = team_key
178
- # Preserve the full teams dict so consumers can introspect siblings.
179
- projection["teams"] = copy.deepcopy(state.get("teams") or {})
180
- if "team_owner" in entry:
181
- projection["team_owner"] = copy.deepcopy(entry["team_owner"])
182
- elif state.get("team_owner") is not None:
183
- projection["team_owner"] = copy.deepcopy(state["team_owner"])
184
- if "leader_receiver" in entry:
185
- projection["leader_receiver"] = copy.deepcopy(entry["leader_receiver"])
186
- elif state.get("leader_receiver") is not None:
187
- projection["leader_receiver"] = copy.deepcopy(state["leader_receiver"])
188
- if "coordinator" in state:
189
- projection.setdefault("coordinator", copy.deepcopy(state["coordinator"]))
190
- return projection
191
-
192
-
193
- def select_runtime_state(workspace: Path, team: str | None = None) -> dict[str, Any]:
194
- state = load_runtime_state(workspace)
195
- alive = team_state_candidates(state)
196
- if team:
197
- if not alive and team in {str(state.get("active_team_key") or ""), team_state_key(state)}:
198
- projection = copy.deepcopy(state)
199
- projection["active_team_key"] = str(team)
200
- return projection
201
- matches = [
202
- (key, value)
203
- for key, value in alive.items()
204
- if team in {key, str(value.get("session_name") or ""), str(value.get("team_dir") or "")}
205
- ]
206
- if len(matches) == 1:
207
- return _project_top_level_view(state, matches[0][0])
208
- from team_agent.errors import RuntimeError
209
- if len(matches) > 1:
210
- raise RuntimeError("team selector is ambiguous. " + format_team_candidates(alive))
211
- raise RuntimeError(f"team {team!r} not found. " + format_team_candidates(alive))
212
- active = state.get("active_team_key")
213
- if active and active in alive:
214
- return _project_top_level_view(state, str(active))
215
- if len(alive) == 1:
216
- return _project_top_level_view(state, next(iter(alive)))
217
- if not alive:
218
- return copy.deepcopy(state)
219
- from team_agent.errors import RuntimeError
220
- raise RuntimeError(
221
- "multiple teams found in this workspace; pass --team <team> to choose. "
222
- + format_team_candidates(alive)
223
- )
224
-
225
-
226
- def ambiguous_team_target_result(state: dict[str, Any]) -> dict[str, Any] | None:
227
- alive = team_state_candidates(state)
228
- active = state.get("active_team_key")
229
- if active and active in alive:
230
- return None
231
- if len(alive) <= 1:
232
- return None
233
- return {
234
- "ok": False,
235
- "status": "refused",
236
- "reason": "team_target_ambiguous",
237
- "candidates": sorted(alive.keys()),
238
- "message": "multiple teams found in this workspace; pass --team <team> to choose. "
239
- + format_team_candidates(alive),
240
- }
241
-
242
-
243
- def resolve_team_scoped_state(
244
- workspace: Path,
245
- team: str | None,
246
- ) -> tuple[dict[str, Any] | None, dict[str, Any] | None]:
247
- if team is None:
248
- ambiguous = ambiguous_team_target_result(load_runtime_state(workspace))
249
- if ambiguous:
250
- return None, ambiguous
251
- try:
252
- from team_agent.errors import RuntimeError as _TeamAgentRuntimeError
253
- return select_runtime_state(workspace, team), None
254
- except _TeamAgentRuntimeError as exc:
255
- return None, {
256
- "ok": False,
257
- "status": "refused",
258
- "reason": "team_target_unresolved",
259
- "team": team,
260
- "error": str(exc),
261
- }
262
-
263
-
264
- def _identity_workspace_abspath(state: dict[str, Any], workspace: Path | None = None) -> str:
265
- if state.get("workspace"):
266
- return str(Path(str(state["workspace"])).resolve())
267
- if state.get("team_dir"):
268
- return str(Path(str(state["team_dir"])).resolve().parent.parent)
269
- if state.get("spec_path"):
270
- spec_path = Path(str(state["spec_path"])).resolve()
271
- return str(spec_path.parent.parent.parent if spec_path.parent.parent.name == ".team" else spec_path.parent)
272
- return str((workspace or Path(os.environ.get("TEAM_AGENT_WORKSPACE") or os.getcwd())).resolve())
273
-
274
-
275
- def _identity_os_user() -> str:
276
- return os.environ.get("USER") or os.environ.get("USERNAME") or ""
277
-
278
-
279
- def _identity_machine_fingerprint(state: dict[str, Any]) -> str:
280
- for record in (state.get("team_owner"), state.get("leader_receiver")):
281
- if isinstance(record, dict) and record.get("machine_fingerprint"):
282
- return str(record["machine_fingerprint"])
283
- return os.environ.get("TEAM_AGENT_MACHINE_FINGERPRINT") or ""
284
-
285
-
286
- def _leader_session_uuid_for_state(state: dict[str, Any], workspace: Path | None = None, team_id: str | None = None) -> str:
287
- return derive_leader_session_uuid(
288
- _identity_machine_fingerprint(state),
289
- _identity_workspace_abspath(state, workspace),
290
- _identity_os_user(),
291
- team_id or team_state_key(state),
292
- )
293
-
294
-
295
- def _migrate_team_identity(state: dict[str, Any], workspace: Path, team_id: str | None = None) -> bool:
296
- leader_uuid = _leader_session_uuid_for_state(state, workspace, team_id)
297
- changed = False
298
- for key in ("team_owner", "leader_receiver"):
299
- record = state.get(key)
300
- if isinstance(record, dict) and not record.get("leader_session_uuid"):
301
- record["leader_session_uuid"] = leader_uuid
302
- changed = True
303
- return changed
304
-
305
-
306
- def _migrate_state_identity(state: dict[str, Any], workspace: Path) -> bool:
307
- changed = _migrate_team_identity(state, workspace) if state.get("session_name") else False
308
- teams = state.get("teams")
309
- if isinstance(teams, dict):
310
- for team_id, team_state in teams.items():
311
- if isinstance(team_state, dict):
312
- changed = _migrate_team_identity(team_state, workspace, str(team_id)) or changed
313
- return changed
314
-
315
-
316
- def _caller_identity_from_env(state: dict[str, Any] | None = None, team_id: str | None = None, workspace: Path | None = None) -> dict[str, str]:
317
- state = state or {}
318
- machine_fingerprint = os.environ.get("TEAM_AGENT_MACHINE_FINGERPRINT") or ""
319
- override = os.environ.get("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE") or ""
320
- env_uuid = os.environ.get("TEAM_AGENT_LEADER_SESSION_UUID") or ""
321
- leader_uuid = override or env_uuid or derive_leader_session_uuid(
322
- machine_fingerprint,
323
- _identity_workspace_abspath(state, workspace),
324
- _identity_os_user(),
325
- team_id or os.environ.get("TEAM_AGENT_TEAM_ID") or team_state_key(state),
326
- )
327
- return {
328
- "pane_id": os.environ.get("TEAM_AGENT_LEADER_PANE_ID") or os.environ.get("TMUX_PANE") or "",
329
- "provider": os.environ.get("TEAM_AGENT_LEADER_PROVIDER") or "",
330
- "machine_fingerprint": machine_fingerprint,
331
- "leader_session_uuid": leader_uuid,
332
- "leader_session_uuid_source": "explicit-override" if override else ("env" if env_uuid else "derived"),
333
- }
334
-
335
-
336
- _TMUX_PANE_LIVE = "live"
337
- _TMUX_PANE_DEAD = "dead"
338
- _TMUX_PANE_UNKNOWN = "unknown"
339
-
340
-
341
- def _tmux_pane_liveness(pane_id: str) -> str:
342
- if not pane_id:
343
- return _TMUX_PANE_UNKNOWN
344
- try:
345
- from team_agent.runtime import run_cmd
346
- proc = run_cmd(["tmux", "display-message", "-p", "-t", pane_id, "#{pane_id}"], timeout=3)
347
- except Exception:
348
- try:
349
- proc = subprocess.run(
350
- ["tmux", "display-message", "-p", "-t", pane_id, "#{pane_id}"],
351
- text=True,
352
- capture_output=True,
353
- timeout=3,
354
- check=False,
355
- )
356
- except Exception:
357
- return _TMUX_PANE_UNKNOWN
358
- if proc.returncode == 0:
359
- return _TMUX_PANE_LIVE
360
- stderr = str(getattr(proc, "stderr", "") or "").lower()
361
- if "can't find pane" in stderr or "can't find window" in stderr or "can't find session" in stderr:
362
- return _TMUX_PANE_DEAD
363
- return _TMUX_PANE_UNKNOWN
364
-
365
-
366
- def check_team_owner(state: dict[str, Any]) -> dict[str, Any] | None:
367
- owner = state.get("team_owner") or {}
368
- if not owner:
369
- return None
370
- _migrate_team_identity(state, Path(_identity_workspace_abspath(state)), team_state_key(state))
371
- caller = _caller_identity_from_env(state, team_state_key(state))
372
- owner_uuid = str(owner.get("leader_session_uuid") or "")
373
- caller_uuid = caller["leader_session_uuid"]
374
- owner_pane = str(owner.get("pane_id") or "")
375
- caller_pane = caller.get("pane_id") or ""
376
- if caller_pane and caller_pane == owner_pane:
377
- return None
378
- if (
379
- caller_pane
380
- and not os.environ.get("TEAM_AGENT_ID")
381
- and owner_pane
382
- and _tmux_pane_liveness(owner_pane) != _TMUX_PANE_LIVE
383
- ):
384
- return None
385
- if caller_uuid == owner_uuid and (not caller_pane or caller_pane == owner_pane):
386
- return None
387
- same_uuid = caller_uuid == owner_uuid
388
- return {
389
- "ok": False,
390
- "status": "refused",
391
- "reason": "team_owner_mismatch",
392
- "reason_kind": "sticky_bind_collision" if same_uuid else "owner_takeover_required",
393
- "error": "not_owner",
394
- "action": "team-agent claim-leader --confirm" if same_uuid else "team-agent takeover --confirm",
395
- "team_owner": owner,
396
- "caller": caller,
397
- }
398
-
399
-
400
- def worker_sender_bypasses_owner_gate(state: dict[str, Any], sender: str | None) -> str | None:
401
- if not sender:
402
- return None
403
- leader_id = (state.get("leader") or {}).get("id") or "leader"
404
- if sender == leader_id or sender in {"leader", "Leader"}:
405
- return None
406
- if sender not in (state.get("agents") or {}):
407
- return None
408
- env_agent_id = os.environ.get("TEAM_AGENT_ID") or ""
409
- if env_agent_id and env_agent_id != sender:
410
- return None
411
- return env_agent_id or sender
412
-
413
-
414
- def populate_team_owner_from_env(state: dict[str, Any], source: str = "autopopulate") -> dict[str, Any] | None:
415
- # Lease mutation convergence marker: _write_lease_dual_state.
416
- if state.get("team_owner"):
417
- _migrate_team_identity(state, Path(_identity_workspace_abspath(state)), team_state_key(state))
418
- return state["team_owner"]
419
- caller = _caller_identity_from_env(state, team_state_key(state))
420
- if not caller["pane_id"]:
421
- return None
422
- owner = {
423
- "pane_id": caller["pane_id"],
424
- "provider": caller["provider"],
425
- "machine_fingerprint": caller["machine_fingerprint"],
426
- "leader_session_uuid": caller["leader_session_uuid"],
427
- "claimed_at": datetime.now(timezone.utc).isoformat(),
428
- "claimed_via": source,
429
- }
430
- state["team_owner"] = owner
431
- return owner
432
-
433
-
434
- def apply_first_time_leader_binding(
435
- workspace: Path,
436
- state: dict[str, Any],
437
- receiver: dict[str, Any],
438
- pane_info: dict[str, Any],
439
- identity: dict[str, Any],
440
- source: str,
441
- ) -> dict[str, Any]:
442
- # Lease mutation convergence marker: _write_lease_dual_state.
443
- from team_agent.messaging.leader_panes import _leader_command_looks_usable
444
- command = pane_info.get("pane_current_command", "")
445
- provider = str(receiver.get("provider") or "")
446
- if not _leader_command_looks_usable(command, provider):
447
- return {"ok": False, "reason": "leader_pane_wrong_command", "error": f"pane command {command!r} is not a leader host", "pane": pane_info}
448
- current_path = pane_info.get("pane_current_path")
449
- if not current_path or os.path.realpath(current_path) != os.path.realpath(str(workspace.resolve())):
450
- return {"ok": False, "reason": "leader_pane_wrong_workspace", "error": f"pane cwd {current_path!r} does not match workspace {str(workspace.resolve())!r}", "pane": pane_info}
451
- receiver.update({
452
- "leader_session_uuid": identity["leader_session_uuid"],
453
- "machine_fingerprint": identity["machine_fingerprint"],
454
- "owner_epoch": 0,
455
- })
456
- state["team_owner"] = {
457
- "pane_id": receiver["pane_id"],
458
- "provider": provider,
459
- "machine_fingerprint": identity["machine_fingerprint"],
460
- "leader_session_uuid": identity["leader_session_uuid"],
461
- "owner_epoch": 0,
462
- "claimed_at": datetime.now(timezone.utc).isoformat(),
463
- "claimed_via": source,
464
- }
465
- state["leader_receiver"] = receiver
466
- return {"ok": True, "pane": pane_info, "warning": None, "first_time": True}
467
-
468
-
469
- def leader_env_exports(receiver: dict[str, Any], identity: dict[str, Any]) -> dict[str, str]:
470
- return {
471
- "TEAM_AGENT_LEADER_PANE_ID": str(receiver.get("pane_id") or ""),
472
- "TEAM_AGENT_LEADER_PROVIDER": str(receiver.get("provider") or ""),
473
- "TEAM_AGENT_LEADER_SESSION_UUID": str(identity.get("leader_session_uuid") or ""),
474
- "TEAM_AGENT_MACHINE_FINGERPRINT": str(identity.get("machine_fingerprint") or ""),
475
- "TEAM_AGENT_WORKSPACE": str(identity.get("workspace_abspath") or ""),
476
- "TEAM_AGENT_TEAM_ID": str(identity.get("team_id") or ""),
477
- }
478
-
479
-
480
- def validate_leader_uuid_from_targets(receiver: dict[str, Any], targets: dict[str, Any]) -> dict[str, Any]:
481
- if receiver.get("provider") == "fake":
482
- return {"ok": True}
483
- if not targets.get("ok"):
484
- return {"ok": False, "reason": "leader_uuid_lookup_failed", "error": targets.get("error") or "tmux target scan failed"}
485
- pane_id = receiver.get("pane_id")
486
- target = next((item for item in targets.get("targets", []) if item.get("pane_id") == pane_id), None)
487
- if not target:
488
- return {"ok": False, "reason": "leader_pane_missing", "error": "tmux pane does not exist"}
489
- return {"ok": True, "pane": target}
490
-
491
-
492
- def save_runtime_state(workspace: Path, state: dict[str, Any]) -> None:
493
- path = runtime_state_path(workspace)
494
- cached = _RUNTIME_STATE_CACHE.get(str(path))
495
- if cached is not None and state == cached:
496
- return
497
- _migrate_state_identity(state, workspace)
498
- cached = _RUNTIME_STATE_CACHE.get(str(path))
499
- if cached is not None and state == cached:
500
- return
501
- if path.exists():
502
- try:
503
- existing = json.loads(path.read_text(encoding="utf-8"))
504
- normalize_agent_session_state(existing)
505
- _migrate_state_identity(existing, workspace)
506
- if state == existing:
507
- _RUNTIME_STATE_CACHE[str(path)] = copy.deepcopy(state)
508
- return
509
- except Exception:
510
- pass
511
- from team_agent.runtime import _runtime_lock
512
- with _runtime_lock(workspace, "state-save", timeout=2.0):
513
- path.parent.mkdir(parents=True, exist_ok=True)
514
- payload = json.dumps(state, indent=2, ensure_ascii=False)
515
- delays = [0.05, 0.2, 0.5]
516
- for attempt in range(len(delays) + 1):
517
- tmp_path = path.with_name(f"{path.name}.{os.getpid()}.{uuid.uuid4().hex}.tmp")
518
- try:
519
- tmp_path.write_text(payload, encoding="utf-8")
520
- os.replace(tmp_path, path)
521
- _RUNTIME_STATE_CACHE[str(path)] = copy.deepcopy(state)
522
- return
523
- except (PermissionError, OSError) as exc:
524
- if not _retryable_replace_error(exc) or attempt >= len(delays):
525
- if _retryable_replace_error(exc):
526
- _self_heal_runtime_state(workspace, path, payload, state, attempt + 1, exc)
527
- return
528
- raise
529
- from team_agent.events import EventLog
530
- EventLog(workspace).write(
531
- "runtime.state.save_retry",
532
- attempt=attempt + 1,
533
- errno=getattr(exc, "errno", None),
534
- errno_name=errno.errorcode.get(getattr(exc, "errno", 0), None),
535
- error=str(exc),
536
- )
537
- time.sleep(delays[attempt])
538
- finally:
539
- tmp_path.unlink(missing_ok=True)
540
-
541
-
542
- def _retryable_replace_error(exc: BaseException) -> bool:
543
- return isinstance(exc, PermissionError) or (
544
- isinstance(exc, OSError) and getattr(exc, "errno", None) in {errno.EACCES, errno.EPERM, errno.EBUSY}
545
- )
546
-
547
-
548
- def _self_heal_runtime_state(
549
- workspace: Path,
550
- path: Path,
551
- payload: str,
552
- state: dict[str, Any],
553
- attempts_used: int,
554
- original_exc: BaseException,
555
- ) -> None:
556
- from team_agent.events import EventLog
557
- event_log = EventLog(workspace)
558
- heal_tmp = path.with_name(f"{path.name}.{os.getpid()}.{uuid.uuid4().hex}.heal.tmp")
559
- backup = path.with_name(f"{path.name}.bak.{os.getpid()}")
560
- backup_created = False
561
- try:
562
- heal_tmp.write_text(payload, encoding="utf-8")
563
- try:
564
- os.replace(path, backup)
565
- backup_created = True
566
- except FileNotFoundError:
567
- backup_created = False
568
- os.replace(heal_tmp, path)
569
- _RUNTIME_STATE_CACHE[str(path)] = copy.deepcopy(state)
570
- event_log.write(
571
- "runtime.state.self_healed",
572
- inode_rebuilt=True,
573
- attempts_used=attempts_used,
574
- replace_retries=max(0, attempts_used - 1),
575
- )
576
- except Exception as exc:
577
- if backup_created:
578
- try:
579
- os.replace(backup, path)
580
- except Exception as restore_exc:
581
- event_log.write("runtime.state.self_heal_restore_failed", error=str(restore_exc))
582
- event_log.write(
583
- "runtime.state.save_failed",
584
- phase="save_runtime_state",
585
- final_errno=getattr(exc, "errno", getattr(original_exc, "errno", None)),
586
- error=str(exc),
587
- retries_used=max(0, attempts_used - 1),
588
- )
589
- raise
590
- finally:
591
- heal_tmp.unlink(missing_ok=True)
592
-
593
-
594
- def save_team_scoped_state(workspace: Path, team_state: dict[str, Any]) -> None:
595
- target_key = team_state_key(team_state)
596
- existing = load_runtime_state(workspace)
597
- existing_primary_key = team_state_key(existing) if existing.get("session_name") else None
598
- if (
599
- existing_primary_key is not None
600
- and existing_primary_key != target_key
601
- and existing.get("session_name")
602
- and existing.get("session_name") == team_state.get("session_name")
603
- ):
604
- existing_primary_key = target_key
605
- existing_teams = existing.get("teams") or {}
606
- incoming_teams = team_state.get("teams") if isinstance(team_state.get("teams"), dict) else None
607
- if not existing_teams and existing_primary_key == target_key:
608
- merged = copy.deepcopy(team_state)
609
- merged.pop("teams", None)
610
- save_runtime_state(workspace, merged)
611
- return
612
- teams = copy.deepcopy(incoming_teams or existing_teams)
613
- teams[target_key] = compact_team_state(team_state)
614
- if existing_primary_key is None or existing_primary_key == target_key:
615
- merged = copy.deepcopy(team_state)
616
- merged["teams"] = teams
617
- else:
618
- merged = copy.deepcopy(existing)
619
- merged["teams"] = teams
620
- if not merged.get("teams"):
621
- merged.pop("teams", None)
622
- save_runtime_state(workspace, merged)
623
-
624
-
625
- def write_team_state(workspace: Path, spec: dict[str, Any], runtime: dict[str, Any], results: list[dict[str, Any]] | None = None) -> Path:
626
- path = workspace / spec.get("context", {}).get("state_file", "team_state.md")
627
- path.parent.mkdir(parents=True, exist_ok=True)
628
- lines = [
629
- "# Team State",
630
- "",
631
- f"Updated: {datetime.now(timezone.utc).isoformat()}",
632
- "",
633
- "## Objective",
634
- "",
635
- spec.get("team", {}).get("objective", ""),
636
- "",
637
- "## Team",
638
- "",
639
- f"- Name: {spec.get('team', {}).get('name')}",
640
- f"- Runtime session: {runtime.get('session_name')}",
641
- ]
642
- receiver = runtime.get("leader_receiver") or {}
643
- if receiver:
644
- if receiver.get("mode") == "direct_tmux":
645
- lines.append(
646
- f"- Leader receiver: direct tmux {receiver.get('pane_id')} "
647
- f"({receiver.get('provider')}, {receiver.get('status')})"
648
- )
649
- else:
650
- lines.append(f"- Leader inbox fallback: {receiver.get('session')}:{receiver.get('window')} ({receiver.get('status')})")
651
- lines.append(f"- Leader inbox log: {receiver.get('path')}")
652
- lines.extend(["", "## Agents", ""])
653
- for agent in spec.get("agents", []):
654
- status = runtime.get("agents", {}).get(agent["id"], {}).get("status", "unknown")
655
- lines.append(f"- {agent['id']}: {agent['role']} on {agent['provider']} ({status})")
656
- lines.extend(["", "## Task Graph", ""])
657
- for task in runtime.get("tasks", spec.get("tasks", [])):
658
- deps = ", ".join(task.get("deps", [])) or "none"
659
- assignee = task.get("assignee") or "unassigned"
660
- lines.append(f"- {task['id']} [{task.get('status', 'pending')}], assignee={assignee}, deps={deps}: {task['title']}")
661
- if task.get("last_result_summary"):
662
- lines.append(f" Summary: {task['last_result_summary']}")
663
- if task.get("artifact_refs"):
664
- for ref in task["artifact_refs"]:
665
- if isinstance(ref, dict):
666
- lines.append(f" Artifact: {ref.get('path')} - {ref.get('description', '')}")
667
- else:
668
- lines.append(f" Artifact: INVALID artifact ref {ref!r}")
669
- lines.extend(["", "## Latest Results", ""])
670
- for result in results or []:
671
- envelope = json.loads(result["envelope"]) if isinstance(result.get("envelope"), str) else result
672
- lines.append(f"- {envelope.get('task_id')} from {envelope.get('agent_id')}: {envelope.get('status')} - {envelope.get('summary')}")
673
- lines.extend(["", "## Blockers", ""])
674
- blockers = [
675
- task
676
- for task in runtime.get("tasks", spec.get("tasks", []))
677
- if task.get("status") in {"blocked", "failed", "needs_retry"}
678
- ]
679
- if blockers:
680
- for task in blockers:
681
- lines.append(f"- {task['id']}: {task.get('last_result_summary', task.get('title'))}")
682
- else:
683
- lines.append("- None")
684
- lines.extend(["", "## Next Step", "", "- Continue routing ready tasks and collect result envelopes."])
685
- path.write_text("\n".join(lines) + "\n", encoding="utf-8")
686
- return path
687
-
688
-
689
- def write_spec(path: Path, spec: dict[str, Any]) -> None:
690
- path.parent.mkdir(parents=True, exist_ok=True)
691
- tmp_path = path.with_suffix(path.suffix + ".tmp")
692
- tmp_path.write_text(dumps(spec), encoding="utf-8")
693
- os.replace(tmp_path, path)