@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,1343 @@
1
+ //! cli · adapters — 每子命令的薄壳 `cmd_*`(commands.py)。委派 status/lifecycle/diagnose/
2
+ //! leader/messaging port,把委派结果包成 [`CmdResult`]。含逻辑的:`cmd_status`(三态互斥)、
3
+ //! `cmd_doctor`(gate/comms/fix-schema/cleanup-orphans 分派)。
4
+
5
+ use super::*;
6
+ use crate::transport::Transport;
7
+
8
+ const INIT_SPEC_TEMPLATE: &str = include_str!("../model/testdata/team.spec.yaml");
9
+ const INIT_STATE_TEMPLATE: &str = r#"# Team State
10
+
11
+ Updated: not launched
12
+
13
+ ## Objective
14
+
15
+ Pending.
16
+
17
+ ## Team
18
+
19
+ - Name: pending
20
+ - Runtime session: pending
21
+
22
+ ## Agents
23
+
24
+ - Pending launch.
25
+
26
+ ## Task Graph
27
+
28
+ - Pending task graph.
29
+
30
+ ## Latest Results
31
+
32
+ - None.
33
+
34
+ ## Blockers
35
+
36
+ - None.
37
+
38
+ ## Next Step
39
+
40
+ - Run `team-agent validate team.spec.yaml`, review permissions, then run `team-agent launch team.spec.yaml --yes`.
41
+ "#;
42
+
43
+ pub fn cmd_init(args: &InitArgs) -> Result<CmdResult, CliError> {
44
+ let team_root = args.workspace.join(".team");
45
+ let spec_path = team_root.join("current").join("team.spec.yaml");
46
+ let state_path = args.workspace.join("team_state.md");
47
+ if spec_path.exists() && !args.force {
48
+ return Err(CliError::Runtime(format!(
49
+ "{} already exists; pass --force to overwrite",
50
+ spec_path.display()
51
+ )));
52
+ }
53
+ for dir in [
54
+ team_root.clone(),
55
+ team_root.join("current"),
56
+ team_root.join("runtime"),
57
+ team_root.join("logs"),
58
+ team_root.join("messages"),
59
+ team_root.join("artifacts"),
60
+ ] {
61
+ std::fs::create_dir_all(&dir)?;
62
+ }
63
+ std::fs::write(&spec_path, INIT_SPEC_TEMPLATE)?;
64
+ if args.force || !state_path.exists() {
65
+ std::fs::write(&state_path, INIT_STATE_TEMPLATE)?;
66
+ }
67
+ crate::event_log::EventLog::new(&args.workspace).write(
68
+ "init",
69
+ json!({
70
+ "spec_path": spec_path.to_string_lossy().to_string(),
71
+ "state_path": state_path.to_string_lossy().to_string(),
72
+ }),
73
+ )
74
+ .map_err(|e| CliError::Runtime(e.to_string()))?;
75
+ Ok(CmdResult::from_json(
76
+ json!({
77
+ "ok": true,
78
+ "spec": spec_path.to_string_lossy().to_string(),
79
+ "state": state_path.to_string_lossy().to_string(),
80
+ }),
81
+ args.json,
82
+ ))
83
+ }
84
+
85
+ /// `cmd_quick_start`(`commands.py:18`)。`--json` 或 `!ok` → 整 dict;否则 `result["summary"]`。
86
+ pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
87
+ let value = lifecycle_port::quick_start(
88
+ &args.agents_dir,
89
+ &args.agents_dir,
90
+ args.name.as_deref(),
91
+ args.team_id.as_deref(),
92
+ args.yes,
93
+ args.fresh,
94
+ )?;
95
+ if args.json || value.get("ok").and_then(Value::as_bool) == Some(false) {
96
+ Ok(CmdResult::from_json(value, args.json))
97
+ } else {
98
+ Ok(CmdResult::human(
99
+ value
100
+ .get("summary")
101
+ .and_then(Value::as_str)
102
+ .unwrap_or("quick-start complete"),
103
+ ))
104
+ }
105
+ }
106
+
107
+ /// `cmd_compile`(`commands.py:42`)。
108
+ pub fn cmd_compile(args: &CompileArgs) -> Result<CmdResult, CliError> {
109
+ let spec = crate::compiler::compile_team(&args.team)
110
+ .map_err(|e| CliError::Runtime(e.to_string()))?;
111
+ std::fs::write(&args.out, crate::model::yaml::dumps(&spec))?;
112
+ Ok(CmdResult::from_json(
113
+ json!({
114
+ "ok": true,
115
+ "team_dir": args.team.to_string_lossy().to_string(),
116
+ "out": args.out.to_string_lossy().to_string(),
117
+ "agents": compiled_agent_ids_for_cli(&spec),
118
+ }),
119
+ args.json,
120
+ ))
121
+ }
122
+
123
+ /// `cmd_status`(`commands.py:90`)。三态:`--summary`(xor json,xor agent)→五行文本;
124
+ /// `--json`→`status_port::status(compact=!detail)`;else→`status_port::format_status(agent)`。
125
+ pub fn cmd_status(args: &StatusArgs) -> Result<CmdResult, CliError> {
126
+ if args.summary && args.json {
127
+ return Err(CliError::Runtime(
128
+ "--summary and --json are mutually exclusive".to_string(),
129
+ ));
130
+ }
131
+ if args.summary && args.agent.is_some() {
132
+ return Err(CliError::Runtime(
133
+ "status --summary does not accept an agent argument".to_string(),
134
+ ));
135
+ }
136
+ let selected = match crate::state::selector::resolve_active_team(
137
+ &args.workspace,
138
+ None,
139
+ crate::state::selector::SelectorMode::RuntimeOnly,
140
+ ) {
141
+ Ok(selected) => selected,
142
+ Err(error) => {
143
+ return Ok(CmdResult::from_json(
144
+ status_selector_error_payload(&error.to_string(), &args.workspace),
145
+ args.json,
146
+ ));
147
+ }
148
+ };
149
+ if args.summary {
150
+ let value = status_port::status(&selected.run_workspace, true, false)?;
151
+ return Ok(CmdResult::human(format_status_summary(&value)));
152
+ }
153
+ if args.json {
154
+ let value = status_port::status(&selected.run_workspace, status_compact_flag(args.detail), args.detail)?;
155
+ return Ok(CmdResult::from_json(value, true));
156
+ }
157
+ Ok(CmdResult::human(status_port::format_status(
158
+ &selected.run_workspace,
159
+ args.agent.as_deref(),
160
+ )?))
161
+ }
162
+
163
+ fn status_selector_error_payload(error: &str, workspace: &Path) -> Value {
164
+ let stamp = chrono::Utc::now().format("%Y%m%d-%H%M%S%.6f");
165
+ let log_path = std::env::temp_dir()
166
+ .join("team-agent")
167
+ .join("cli-errors")
168
+ .join(format!("status-{stamp}.log"));
169
+ if let Some(parent) = log_path.parent() {
170
+ let _ = std::fs::create_dir_all(parent);
171
+ }
172
+ let _ = std::fs::write(&log_path, format!("{error}\n"));
173
+ json!({
174
+ "ok": false,
175
+ "error": error,
176
+ "action": "run `team-agent doctor` or inspect the log path shown here",
177
+ "log": log_path.to_string_lossy().to_string(),
178
+ "workspace": workspace.to_string_lossy().to_string(),
179
+ })
180
+ }
181
+
182
+ /// `cmd_watch`(`commands.py:103`)。委派 `coordinator::run_watch`;KeyboardInterrupt/正常 → `SystemExit(0)`。
183
+ /// 返回 [`CmdResult::none`](不经 emit)。
184
+ pub fn cmd_watch(args: &WatchArgs) -> Result<CmdResult, CliError> {
185
+ let workspace = crate::coordinator::WorkspacePath::new(args.workspace.clone());
186
+ #[cfg(not(test))]
187
+ {
188
+ let mut output = |line: &str| {
189
+ println!("{line}");
190
+ let _ = std::io::Write::flush(&mut std::io::stdout());
191
+ };
192
+ crate::coordinator::run_watch(&workspace, args.team.as_deref(), 1.0, &mut output)
193
+ .map_err(|e| CliError::Runtime(e.to_string()))?;
194
+ Ok(CmdResult::none())
195
+ }
196
+ #[cfg(test)]
197
+ {
198
+ let store = crate::message_store::MessageStore::open(&args.workspace)
199
+ .map_err(|e| CliError::Runtime(e.to_string()))?;
200
+ let mut cursor = crate::coordinator::WatchCursor::default();
201
+ let lines = crate::coordinator::collect_watch_lines(
202
+ &workspace,
203
+ &mut cursor,
204
+ &store,
205
+ args.team.as_deref(),
206
+ )
207
+ .map_err(|e| CliError::Runtime(e.to_string()))?;
208
+ Ok(CmdResult::human(lines.join("\n")))
209
+ }
210
+ }
211
+
212
+ /// `cmd_sessions`(`parser.py:230`)。
213
+ pub fn cmd_sessions(args: &SessionsArgs) -> Result<CmdResult, CliError> {
214
+ let state = crate::state::persist::load_runtime_state(&args.workspace)?;
215
+ let spec = load_team_spec_optional(&args.workspace, &state)?;
216
+ Ok(CmdResult::from_json(
217
+ json!({
218
+ "ok": true,
219
+ "sessions": sessions_overview(&state, spec.as_ref()),
220
+ "workspace": args.workspace.to_string_lossy().to_string(),
221
+ }),
222
+ args.json,
223
+ ))
224
+ }
225
+
226
+ /// `cmd_validate_result`(`commands.py:206`)。
227
+ pub fn cmd_validate_result(args: &ValidateResultArgs) -> Result<CmdResult, CliError> {
228
+ let raw = if let Some(path) = &args.file {
229
+ std::fs::read_to_string(path)?
230
+ } else if let Some(envelope) = &args.envelope {
231
+ envelope.clone()
232
+ } else if let Some(result) = &args.result {
233
+ result.clone()
234
+ } else {
235
+ let mut input = String::new();
236
+ std::io::Read::read_to_string(&mut std::io::stdin(), &mut input)?;
237
+ input
238
+ };
239
+ let envelope: Value = serde_json::from_str(&raw)?;
240
+ crate::model::spec::validate_result_envelope(&envelope)
241
+ .map_err(|e| CliError::Runtime(e.to_string()))?;
242
+ Ok(CmdResult::from_json(
243
+ json!({
244
+ "ok": true,
245
+ "task_id": envelope.get("task_id").cloned().unwrap_or(Value::Null),
246
+ "agent_id": envelope.get("agent_id").cloned().unwrap_or(Value::Null),
247
+ "status": envelope.get("status").cloned().unwrap_or(Value::Null),
248
+ }),
249
+ args.json,
250
+ ))
251
+ }
252
+
253
+ /// `cmd_collect`(`parser.py:292`)。
254
+ pub fn cmd_collect(args: &CollectArgs) -> Result<CmdResult, CliError> {
255
+ let selected = match crate::state::selector::resolve_active_team(
256
+ &args.workspace,
257
+ None,
258
+ crate::state::selector::SelectorMode::RuntimeOnly,
259
+ ) {
260
+ Ok(selected) => selected,
261
+ Err(error) => {
262
+ return Ok(CmdResult::from_json(
263
+ json!({
264
+ "ok": false,
265
+ "error": error.to_string(),
266
+ "workspace": args.workspace.to_string_lossy().to_string(),
267
+ }),
268
+ args.json,
269
+ ));
270
+ }
271
+ };
272
+ let value = match messaging::collect(&selected.run_workspace, args.result_file.as_deref(), false) {
273
+ Ok(value) => value,
274
+ Err(error) => {
275
+ return Ok(CmdResult::from_json(
276
+ json!({
277
+ "ok": false,
278
+ "error": error.to_string(),
279
+ "workspace": selected.run_workspace.to_string_lossy().to_string(),
280
+ }),
281
+ args.json,
282
+ ));
283
+ }
284
+ };
285
+ let results = value.get("results").cloned().unwrap_or_else(|| json!({}));
286
+ let ok = value.get("ok").and_then(Value::as_bool).unwrap_or(true);
287
+ Ok(CmdResult::from_json(
288
+ json!({
289
+ "collected": [],
290
+ "collected_results": value.get("collected_results").cloned().unwrap_or_else(|| json!([])),
291
+ "coordinator": {
292
+ "ok": false,
293
+ "status": "not_required",
294
+ },
295
+ "delivered_messages": value.get("delivered_messages").cloned().unwrap_or_else(|| json!([])),
296
+ "invalid_results": value.get("invalid_results").cloned().unwrap_or_else(|| json!([])),
297
+ "ok": ok,
298
+ "results": results,
299
+ "state_file": value
300
+ .get("state_file")
301
+ .cloned()
302
+ .unwrap_or_else(|| {
303
+ json!(selected
304
+ .spec_workspace
305
+ .as_deref()
306
+ .unwrap_or(&selected.run_workspace)
307
+ .join("team_state.md")
308
+ .to_string_lossy()
309
+ .to_string())
310
+ }),
311
+ }),
312
+ args.json,
313
+ ))
314
+ }
315
+
316
+ /// `cmd_settle`(`commands.py:86`)。
317
+ pub fn cmd_settle(args: &SettleArgs) -> Result<CmdResult, CliError> {
318
+ match settle_value(&args.workspace) {
319
+ Ok(value) => Ok(CmdResult::from_json(value, args.json)),
320
+ Err(error) => Ok(CmdResult::from_json(
321
+ json!({
322
+ "ok": false,
323
+ "error": error.to_string(),
324
+ "workspace": args.workspace.to_string_lossy().to_string(),
325
+ }),
326
+ args.json,
327
+ )),
328
+ }
329
+ }
330
+
331
+ fn settle_value(workspace: &Path) -> Result<Value, CliError> {
332
+ let mut collect = messaging::collect(workspace, None, false)?;
333
+ if collect.get("ok").and_then(Value::as_bool) == Some(false) {
334
+ let message = collect
335
+ .get("error")
336
+ .and_then(Value::as_str)
337
+ .unwrap_or("collect failed");
338
+ return Err(CliError::Runtime(message.to_string()));
339
+ }
340
+ let coordinator_log = crate::coordinator::coordinator_log_path(
341
+ &crate::coordinator::WorkspacePath::new(workspace.to_path_buf()),
342
+ );
343
+ let collect_object = collect
344
+ .as_object_mut()
345
+ .ok_or_else(|| CliError::Runtime("collect returned non-object output".to_string()))?;
346
+ collect_object.insert(
347
+ "coordinator".to_string(),
348
+ json!({
349
+ "ok": true,
350
+ "status": "started",
351
+ "log": coordinator_log.to_string_lossy().to_string(),
352
+ }),
353
+ );
354
+ let status = status_port::status(workspace, true, false)?;
355
+ let details_log = write_settle_details_log(workspace, &collect, &status)?;
356
+ let collected_count = collect
357
+ .get("collected")
358
+ .and_then(Value::as_array)
359
+ .map_or(0, Vec::len);
360
+ Ok(json!({
361
+ "ok": true,
362
+ "summary": format!("collected {collected_count} result(s)"),
363
+ "next_actions": ["Review team_state.md and decide whether to continue or shutdown."],
364
+ "details_log": details_log.to_string_lossy().to_string(),
365
+ "collect": collect,
366
+ }))
367
+ }
368
+
369
+ fn write_settle_details_log(workspace: &Path, collect: &Value, status: &Value) -> Result<PathBuf, CliError> {
370
+ let logs = workspace.join(".team").join("logs");
371
+ std::fs::create_dir_all(&logs)?;
372
+ let timestamp = std::time::SystemTime::now()
373
+ .duration_since(std::time::UNIX_EPOCH)
374
+ .map_or(0, |duration| duration.as_secs());
375
+ let path = logs.join(format!("settle-{timestamp}.json"));
376
+ let details = json!({
377
+ "collect": collect,
378
+ "status": status,
379
+ });
380
+ let text = serde_json::to_string_pretty(&crate::cli::sort_json(&details))?;
381
+ std::fs::write(&path, text)?;
382
+ Ok(path)
383
+ }
384
+
385
+ /// `cmd_allow_peer_talk`(`parser.py allow-peer-talk`).
386
+ pub fn cmd_allow_peer_talk(args: &AllowPeerTalkArgs) -> Result<CmdResult, CliError> {
387
+ let value = messaging::allow_peer_talk(&args.workspace, &args.a, &args.b)?;
388
+ Ok(CmdResult::from_json(value, args.json))
389
+ }
390
+
391
+ /// `cmd_repair_state`(`parser.py:303`)。
392
+ pub fn cmd_repair_state(args: &RepairStateArgs) -> Result<CmdResult, CliError> {
393
+ if !is_repair_task_status(&args.status) {
394
+ return Err(CliError::Runtime(format!(
395
+ "unknown task status for repair: {}",
396
+ args.status
397
+ )));
398
+ }
399
+ let mut state = crate::state::persist::load_runtime_state(&args.workspace)?;
400
+ let before = find_task_projection(&state, &args.task_id).unwrap_or_else(repair_task_projection_null);
401
+ update_task(
402
+ &mut state,
403
+ &args.task_id,
404
+ args.assignee.as_deref(),
405
+ &args.status,
406
+ args.summary.as_deref(),
407
+ );
408
+ let after = find_task_projection(&state, &args.task_id).unwrap_or_else(repair_task_projection_null);
409
+ crate::state::persist::save_runtime_state(&args.workspace, &state)?;
410
+ let spec = load_team_spec_optional(&args.workspace, &state)?
411
+ .ok_or_else(|| CliError::Runtime("team.spec.yaml not found".to_string()))?;
412
+ let state_file = crate::lifecycle::restart::write_team_state(&args.workspace, &spec, &state)
413
+ .map_err(|e| CliError::Runtime(e.to_string()))?;
414
+ crate::event_log::EventLog::new(&args.workspace)
415
+ .write(
416
+ "repair_state.task",
417
+ json!({
418
+ "task_id": args.task_id,
419
+ "before": before,
420
+ "after": after,
421
+ }),
422
+ )
423
+ .map_err(|e| CliError::Runtime(e.to_string()))?;
424
+ Ok(CmdResult::from_json(
425
+ json!({
426
+ "ok": true,
427
+ "task_id": args.task_id,
428
+ "before": before,
429
+ "after": after,
430
+ "state_file": state_file.to_string_lossy().to_string(),
431
+ }),
432
+ args.json,
433
+ ))
434
+ }
435
+
436
+ /// `cmd_diagnose`(`parser.py:298`)。
437
+ pub fn cmd_diagnose(args: &DiagnoseArgs) -> Result<CmdResult, CliError> {
438
+ let state = crate::state::persist::load_runtime_state(&args.workspace)?;
439
+ let event_log = args.workspace.join(".team").join("logs").join("events.jsonl");
440
+ let backend = crate::tmux_backend::TmuxBackend::for_workspace(&args.workspace);
441
+ let (issues, suggested_repairs) = diagnose_runtime(&state, &backend);
442
+ let ok = issues.as_array().is_some_and(Vec::is_empty);
443
+ Ok(CmdResult::from_json(
444
+ json!({
445
+ "event_log": event_log.to_string_lossy().to_string(),
446
+ "issues": issues,
447
+ "ok": ok,
448
+ "providers": provider_doctor_checks(),
449
+ "runtime": {
450
+ "workspace": args.workspace.to_string_lossy().to_string(),
451
+ "session_name": state.get("session_name").cloned().unwrap_or(Value::Null),
452
+ "leader_receiver": state.get("leader_receiver").cloned().unwrap_or(Value::Null),
453
+ "agent_count": state.get("agents").and_then(Value::as_object).map_or(0, serde_json::Map::len),
454
+ "message_count": count_dir_entries(&args.workspace.join(".team").join("messages")),
455
+ "result_count": count_dir_entries(&args.workspace.join(".team").join("results")),
456
+ },
457
+ "suggested_repairs": suggested_repairs,
458
+ }),
459
+ args.json,
460
+ ))
461
+ }
462
+
463
+ /// `cmd_preflight`(`parser.py:160`)。
464
+ pub fn cmd_preflight(args: &PreflightArgs) -> Result<CmdResult, CliError> {
465
+ let report = build_preflight_report(&args.team)?;
466
+ Ok(CmdResult::from_json(
467
+ report,
468
+ args.json,
469
+ ))
470
+ }
471
+
472
+ /// `cmd_wait_ready`(`parser.py:171`)。
473
+ pub fn cmd_wait_ready(args: &WaitReadyArgs) -> Result<CmdResult, CliError> {
474
+ let report = build_wait_ready_report(&args.workspace, args.timeout)?;
475
+ Ok(CmdResult::from_json(
476
+ report,
477
+ args.json,
478
+ ))
479
+ }
480
+
481
+ /// `cmd_e2e`(`parser.py:449`)。
482
+ pub fn cmd_e2e(args: &E2eArgs) -> Result<CmdResult, CliError> {
483
+ let mut providers = serde_json::Map::new();
484
+ for provider in &args.providers {
485
+ let result = if provider == "fake" {
486
+ run_fake_e2e(&args.workspace)?
487
+ } else {
488
+ skipped_provider_e2e(provider, args.real)
489
+ };
490
+ providers.insert(provider.clone(), result);
491
+ }
492
+ let ok = providers
493
+ .values()
494
+ .all(|value| value.get("ok").and_then(Value::as_bool) == Some(true));
495
+ Ok(CmdResult::from_json(
496
+ json!({
497
+ "workspace": args.workspace.to_string_lossy().to_string(),
498
+ "providers": Value::Object(providers),
499
+ "ok": ok,
500
+ }),
501
+ args.json,
502
+ ))
503
+ }
504
+
505
+ /// `cmd_peek`(`commands.py:118`)。
506
+ pub fn cmd_peek(args: &PeekArgs) -> Result<CmdResult, CliError> {
507
+ if !args.allow_raw_screen {
508
+ return Err(CliError::Usage("peek requires --allow-raw-screen".to_string()));
509
+ }
510
+ let state = crate::state::persist::load_runtime_state(&args.workspace)?;
511
+ let Some(agent_state) = state.get("agents").and_then(|agents| agents.get(&args.agent)) else {
512
+ return Ok(CmdResult::from_json(
513
+ json!({
514
+ "ok": false,
515
+ "agent_id": args.agent,
516
+ "error": format!("unknown agent id: {}", args.agent),
517
+ }),
518
+ args.json,
519
+ ));
520
+ };
521
+ let Some((session, window, target)) = agent_pane_id(&state, &args.agent, agent_state) else {
522
+ return Ok(peek_unavailable(&args.agent, args.json));
523
+ };
524
+ let backend = crate::tmux_backend::TmuxBackend::for_workspace(&args.workspace);
525
+ let windows = backend
526
+ .list_windows(&crate::transport::SessionName::new(session.clone()))
527
+ .map_err(|e| CliError::Runtime(e.to_string()))?;
528
+ if !windows.iter().any(|w| w.as_str() == window) {
529
+ return Ok(peek_unavailable(&args.agent, args.json));
530
+ }
531
+ let capture = backend
532
+ .capture(
533
+ &crate::transport::Target::Pane(crate::transport::PaneId::new(target.clone())),
534
+ crate::transport::CaptureRange::Tail(args.tail as u32),
535
+ )
536
+ .map_err(|e| CliError::Runtime(e.to_string()))?;
537
+ Ok(CmdResult::from_json(
538
+ json!({
539
+ "ok": true,
540
+ "agent_id": args.agent,
541
+ "workspace": args.workspace.to_string_lossy().to_string(),
542
+ "tail": args.tail,
543
+ "pane_id": target,
544
+ "text": capture.text,
545
+ }),
546
+ args.json,
547
+ ))
548
+ }
549
+
550
+ fn peek_unavailable(agent: &str, json: bool) -> CmdResult {
551
+ CmdResult::from_json(
552
+ json!({
553
+ "ok": false,
554
+ "agent_id": agent,
555
+ "error": format!("agent terminal is not available: {agent}"),
556
+ }),
557
+ json,
558
+ )
559
+ }
560
+
561
+ fn agent_pane_id(state: &Value, agent: &str, agent_state: &Value) -> Option<(String, String, String)> {
562
+ let session = state.get("session_name").and_then(Value::as_str).filter(|s| !s.is_empty())?;
563
+ let window = ["window", "window_name"]
564
+ .iter()
565
+ .find_map(|key| agent_state.get(*key).and_then(Value::as_str).filter(|s| !s.is_empty()))
566
+ .unwrap_or(agent);
567
+ Some((session.to_string(), window.to_string(), format!("{session}:{window}")))
568
+ }
569
+
570
+ fn run_fake_e2e(workspace: &Path) -> Result<Value, CliError> {
571
+ std::fs::create_dir_all(workspace)?;
572
+ let spec_path = workspace.join("team.spec.yaml");
573
+ std::fs::write(&spec_path, fake_spec_yaml(workspace))?;
574
+ let launch = match crate::lifecycle::launch(&spec_path, true, true, true) {
575
+ Ok(report) => json!({
576
+ "ok": true,
577
+ "dry_run": report.dry_run,
578
+ "session_name": report.session_name.as_str(),
579
+ }),
580
+ Err(error) => json!({
581
+ "ok": false,
582
+ "error": error.to_string(),
583
+ }),
584
+ };
585
+ seed_fake_e2e_state(workspace)?;
586
+ let send = messaging::send_message(
587
+ workspace,
588
+ &MessageTarget::Single("fake_impl".to_string()),
589
+ "implement fake task",
590
+ &SendOptions {
591
+ task_id: Some(TaskId::new("task_impl")),
592
+ route_task_id: false,
593
+ sender: "leader".to_string(),
594
+ requires_ack: true,
595
+ ..SendOptions::default()
596
+ },
597
+ )?;
598
+ let send_value = json!({
599
+ "ok": send.ok,
600
+ "status": send.status,
601
+ "message_status": send.message_status.0,
602
+ "message_id": send.message_id,
603
+ "reason": send.reason,
604
+ });
605
+ if send.ok {
606
+ let _ = messaging::report_result(
607
+ workspace,
608
+ &json!({
609
+ "schema_version": "result_envelope_v1",
610
+ "task_id": "task_impl",
611
+ "agent_id": "fake_impl",
612
+ "status": "success",
613
+ "summary": "fake result collected",
614
+ "changes": [],
615
+ "tests": [],
616
+ "risks": [],
617
+ "artifacts": [],
618
+ "next_actions": [],
619
+ }),
620
+ )?;
621
+ }
622
+ let mut collect = messaging::collect(workspace, None, false)?;
623
+ let collected = collect
624
+ .get("collected_results")
625
+ .and_then(Value::as_array)
626
+ .is_some_and(|items| !items.is_empty());
627
+ if let Some(obj) = collect.as_object_mut() {
628
+ obj.insert("collected".to_string(), Value::Bool(collected));
629
+ }
630
+ let shutdown = fake_shutdown(workspace)?;
631
+ let ok = launch.get("ok").and_then(Value::as_bool) == Some(true)
632
+ && send.ok
633
+ && collected
634
+ && shutdown.get("ok").and_then(Value::as_bool) == Some(true);
635
+ Ok(json!({
636
+ "ok": ok,
637
+ "launch": launch,
638
+ "send": send_value,
639
+ "collect": collect,
640
+ "shutdown": shutdown,
641
+ }))
642
+ }
643
+
644
+ fn skipped_provider_e2e(provider: &str, real: bool) -> Value {
645
+ let command = provider_command(provider);
646
+ if !command_on_path(command) {
647
+ return json!({
648
+ "ok": false,
649
+ "skipped": true,
650
+ "reason": format!("{command} not installed"),
651
+ "version": null,
652
+ });
653
+ }
654
+ let version = command_version(command);
655
+ if !real {
656
+ return json!({
657
+ "ok": false,
658
+ "skipped": true,
659
+ "reason": "real provider launch disabled; rerun with --real on an authenticated machine",
660
+ "version": version,
661
+ });
662
+ }
663
+ json!({
664
+ "ok": false,
665
+ "skipped": true,
666
+ "reason": "real provider e2e is not available in this build",
667
+ "version": version,
668
+ })
669
+ }
670
+
671
+ fn command_on_path(command: &str) -> bool {
672
+ std::env::var_os("PATH").is_some_and(|paths| {
673
+ std::env::split_paths(&paths).any(|dir| {
674
+ let candidate = dir.join(command);
675
+ candidate.is_file()
676
+ })
677
+ })
678
+ }
679
+
680
+ fn command_version(command: &str) -> Value {
681
+ match std::process::Command::new(command).arg("--version").output() {
682
+ Ok(output) if output.status.success() => {
683
+ let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
684
+ if text.is_empty() {
685
+ Value::Null
686
+ } else {
687
+ Value::String(text)
688
+ }
689
+ }
690
+ _ => Value::Null,
691
+ }
692
+ }
693
+
694
+ fn provider_command(provider: &str) -> &str {
695
+ match provider {
696
+ "claude" | "claude_code" => "claude",
697
+ "gemini" | "gemini_cli" => "gemini",
698
+ other => other,
699
+ }
700
+ }
701
+
702
+ fn seed_fake_e2e_state(workspace: &Path) -> Result<(), CliError> {
703
+ crate::state::persist::save_runtime_state(
704
+ workspace,
705
+ &json!({
706
+ "leader": {"id": "leader"},
707
+ "session_name": "team-agent-fake-e2e",
708
+ "agents": {
709
+ "fake_impl": {
710
+ "provider": "fake",
711
+ "status": "running",
712
+ "window": "fake_impl",
713
+ "pane_id": "%fake_impl",
714
+ "spawn_cwd": workspace.to_string_lossy().to_string(),
715
+ }
716
+ },
717
+ "tasks": [{
718
+ "id": "task_impl",
719
+ "title": "Fake implementation",
720
+ "type": "implementation",
721
+ "assignee": "fake_impl",
722
+ "status": "pending",
723
+ }]
724
+ }),
725
+ )?;
726
+ Ok(())
727
+ }
728
+
729
+ fn fake_shutdown(workspace: &Path) -> Result<Value, CliError> {
730
+ let mut state = crate::state::persist::load_runtime_state(workspace)?;
731
+ if let Some(agents) = state.get_mut("agents").and_then(Value::as_object_mut) {
732
+ for agent in agents.values_mut() {
733
+ if let Some(obj) = agent.as_object_mut() {
734
+ obj.insert("status".to_string(), Value::String("stopped".to_string()));
735
+ }
736
+ }
737
+ }
738
+ crate::state::persist::save_runtime_state(workspace, &state)?;
739
+ Ok(json!({
740
+ "ok": true,
741
+ "session_name": state.get("session_name").cloned().unwrap_or(Value::Null),
742
+ "session_killed": false,
743
+ "coordinator": {"status": "missing", "pid": null},
744
+ }))
745
+ }
746
+
747
+ fn fake_spec_yaml(workspace: &Path) -> String {
748
+ let ws = workspace.to_string_lossy();
749
+ format!(
750
+ r#"version: 1
751
+ team:
752
+ name: "fake-e2e"
753
+ mode: "supervisor_worker"
754
+ objective: "Exercise fake provider orchestration."
755
+ workspace: "{ws}"
756
+ leader:
757
+ id: "leader"
758
+ role: "leader"
759
+ provider: "fake"
760
+ model: null
761
+ tools:
762
+ - "fs_read"
763
+ - "fs_list"
764
+ - "mcp_team"
765
+ context_policy:
766
+ keep_user_thread: true
767
+ receive_worker_outputs: "structured_only"
768
+ max_worker_result_tokens: 2000
769
+ agents:
770
+ - id: "fake_impl"
771
+ role: "implementation_engineer"
772
+ provider: "fake"
773
+ model: null
774
+ working_directory: "{ws}"
775
+ system_prompt:
776
+ inline: "Handle fake implementation tasks."
777
+ file: null
778
+ tools:
779
+ - "fs_read"
780
+ - "fs_write"
781
+ - "fs_list"
782
+ - "execute_bash"
783
+ - "git_diff"
784
+ - "mcp_team"
785
+ - "provider_builtin"
786
+ permission_mode: "restricted"
787
+ preferred_for:
788
+ - "implementation"
789
+ avoid_for: []
790
+ output_contract:
791
+ format: "result_envelope_v1"
792
+ required_fields:
793
+ - "task_id"
794
+ - "status"
795
+ - "summary"
796
+ - "artifacts"
797
+ routing:
798
+ default_assignee: "leader"
799
+ rules:
800
+ - id: "implementation-to-fake"
801
+ match:
802
+ type:
803
+ - "implementation"
804
+ assign_to: "fake_impl"
805
+ priority: 10
806
+ communication:
807
+ protocol: "mcp_inbox"
808
+ topology: "leader_centered"
809
+ worker_to_worker: true
810
+ ack_timeout_sec: 2
811
+ result_format: "result_envelope_v1"
812
+ message_store:
813
+ sqlite: ".team/runtime/team.db"
814
+ mirror_files: ".team/messages"
815
+ runtime:
816
+ backend: "tmux"
817
+ display_backend: "none"
818
+ session_name: "team-agent-fake-e2e"
819
+ auto_launch: true
820
+ require_user_approval_before_launch: false
821
+ max_active_agents: 1
822
+ startup_order:
823
+ - "fake_impl"
824
+ context:
825
+ state_file: "team_state.md"
826
+ artifact_dir: ".team/artifacts"
827
+ log_dir: ".team/logs"
828
+ summarization:
829
+ worker_full_logs: "retain_outside_leader_context"
830
+ state_update: "after_each_result"
831
+ tasks:
832
+ - id: "task_impl"
833
+ title: "Fake implementation"
834
+ type: "implementation"
835
+ assignee: "fake_impl"
836
+ deps: []
837
+ acceptance:
838
+ - "fake result collected"
839
+ status: "pending"
840
+ requires_tools:
841
+ - "fs_write"
842
+ - "execute_bash"
843
+ files:
844
+ - "src/example.py"
845
+ risk: "low"
846
+ "#
847
+ )
848
+ }
849
+
850
+ fn sessions_overview(state: &Value, spec: Option<&crate::model::yaml::Value>) -> Value {
851
+ let mut rows = Vec::new();
852
+ if let Some(agents) = spec.and_then(|v| v.get("agents")).and_then(crate::model::yaml::Value::as_list) {
853
+ for agent in agents {
854
+ let Some(agent_id) = agent.get("id").and_then(crate::model::yaml::Value::as_str) else {
855
+ continue;
856
+ };
857
+ let agent_state = state
858
+ .get("agents")
859
+ .and_then(|agents| agents.get(agent_id))
860
+ .unwrap_or(&Value::Null);
861
+ rows.push(session_row(state, agent, agent_id, agent_state));
862
+ }
863
+ }
864
+ Value::Array(rows)
865
+ }
866
+
867
+ fn compiled_agent_ids_for_cli(spec: &crate::model::yaml::Value) -> Vec<String> {
868
+ spec.get("agents")
869
+ .and_then(crate::model::yaml::Value::as_list)
870
+ .map(|agents| {
871
+ agents
872
+ .iter()
873
+ .filter_map(|agent| agent.get("id").and_then(crate::model::yaml::Value::as_str))
874
+ .map(str::to_string)
875
+ .collect()
876
+ })
877
+ .unwrap_or_default()
878
+ }
879
+
880
+ fn session_row(
881
+ state: &Value,
882
+ spec_agent: &crate::model::yaml::Value,
883
+ agent_id: &str,
884
+ agent_state: &Value,
885
+ ) -> Value {
886
+ json!({
887
+ "agent_id": agent_id,
888
+ "provider": yaml_str(spec_agent, "provider").map(Value::String).unwrap_or(Value::Null),
889
+ "model": yaml_str(spec_agent, "model").map(Value::String).unwrap_or(Value::Null),
890
+ "profile": yaml_str(spec_agent, "profile").map(Value::String).unwrap_or(Value::Null),
891
+ "session_id": field_or_null(agent_state, &["session_id"]),
892
+ "resume_id": field_or_null(agent_state, &["resume_id"]),
893
+ "rollout_path": field_or_null(agent_state, &["rollout_path"]),
894
+ "captured_at": field_or_null(agent_state, &["captured_at"]),
895
+ "captured_via": field_or_null(agent_state, &["captured_via"]),
896
+ "attribution_confidence": field_or_null(agent_state, &["attribution_confidence"]),
897
+ "spawn_cwd": field_or_null(agent_state, &["spawn_cwd", "working_directory"]),
898
+ "context_usage": field_or_null(agent_state, &["context_usage"]),
899
+ "status": field_or_default(agent_state, "status", "unknown"),
900
+ "last_task": last_task_for_agent(state, agent_id),
901
+ "handoff_path": field_or_null(agent_state, &["handoff_path"]),
902
+ "display_target": field_or_null(agent_state, &["display"]),
903
+ "terminal_target": terminal_target(state, agent_id, agent_state),
904
+ })
905
+ }
906
+
907
+ fn yaml_str(value: &crate::model::yaml::Value, key: &str) -> Option<String> {
908
+ value.get(key).and_then(crate::model::yaml::Value::as_str).map(ToString::to_string)
909
+ }
910
+
911
+ fn field_or_null(value: &Value, keys: &[&str]) -> Value {
912
+ keys.iter()
913
+ .find_map(|key| value.get(*key).cloned())
914
+ .unwrap_or(Value::Null)
915
+ }
916
+
917
+ fn field_or_default(value: &Value, key: &str, default: &str) -> Value {
918
+ value
919
+ .get(key)
920
+ .cloned()
921
+ .unwrap_or_else(|| Value::String(default.to_string()))
922
+ }
923
+
924
+ fn last_task_for_agent(state: &Value, agent_id: &str) -> Value {
925
+ state
926
+ .get("tasks")
927
+ .and_then(Value::as_array)
928
+ .and_then(|tasks| {
929
+ tasks
930
+ .iter()
931
+ .rev()
932
+ .find(|task| task.get("assignee").and_then(Value::as_str) == Some(agent_id))
933
+ })
934
+ .and_then(|task| task.get("id").and_then(Value::as_str).map(ToString::to_string))
935
+ .map(Value::String)
936
+ .unwrap_or(Value::Null)
937
+ }
938
+
939
+ fn terminal_target(state: &Value, agent_id: &str, agent_state: &Value) -> Value {
940
+ json!({
941
+ "session": agent_state
942
+ .get("session_name")
943
+ .cloned()
944
+ .or_else(|| state.get("session_name").cloned())
945
+ .unwrap_or(Value::Null),
946
+ "window": window_target(agent_state, agent_id),
947
+ "pane": field_or_null(agent_state, &["pane_id"]),
948
+ })
949
+ }
950
+
951
+ fn window_target(agent_state: &Value, agent_id: &str) -> Value {
952
+ let window = field_or_null(agent_state, &["window", "window_name"]);
953
+ if window.is_null() {
954
+ Value::String(agent_id.to_string())
955
+ } else {
956
+ window
957
+ }
958
+ }
959
+
960
+
961
+ fn find_task_projection(state: &Value, task_id: &str) -> Option<Value> {
962
+ state
963
+ .get("tasks")
964
+ .and_then(Value::as_array)
965
+ .and_then(|tasks| tasks.iter().find(|task| task.get("id").and_then(Value::as_str) == Some(task_id)))
966
+ .map(repair_task_projection)
967
+ }
968
+
969
+ fn update_task(
970
+ state: &mut Value,
971
+ task_id: &str,
972
+ assignee: Option<&str>,
973
+ status: &str,
974
+ summary: Option<&str>,
975
+ ) -> Value {
976
+ if let Some(tasks) = state.get_mut("tasks").and_then(Value::as_array_mut) {
977
+ for task in tasks {
978
+ if task.get("id").and_then(Value::as_str) == Some(task_id) {
979
+ if let Some(obj) = task.as_object_mut() {
980
+ if let Some(assignee) = assignee {
981
+ obj.insert("assignee".to_string(), Value::String(assignee.to_string()));
982
+ }
983
+ obj.insert("status".to_string(), Value::String(status.to_string()));
984
+ if let Some(summary) = summary {
985
+ obj.insert("last_result_summary".to_string(), Value::String(summary.to_string()));
986
+ }
987
+ return Value::Object(obj.clone());
988
+ }
989
+ }
990
+ }
991
+ }
992
+ json!({
993
+ "id": task_id,
994
+ "assignee": assignee.unwrap_or(""),
995
+ "status": status,
996
+ "summary": summary.unwrap_or(""),
997
+ })
998
+ }
999
+
1000
+ fn is_repair_task_status(status: &str) -> bool {
1001
+ matches!(
1002
+ status,
1003
+ "blocked" | "cancelled" | "done" | "failed" | "needs_retry" | "pending" | "ready" | "running"
1004
+ )
1005
+ }
1006
+
1007
+ fn repair_task_projection(task: &Value) -> Value {
1008
+ let mut map = serde_json::Map::new();
1009
+ map.insert(
1010
+ "assignee".to_string(),
1011
+ task.get("assignee").cloned().unwrap_or(Value::Null),
1012
+ );
1013
+ map.insert(
1014
+ "status".to_string(),
1015
+ task.get("status").cloned().unwrap_or(Value::Null),
1016
+ );
1017
+ map.insert(
1018
+ "last_result_summary".to_string(),
1019
+ task.get("last_result_summary").cloned().unwrap_or(Value::Null),
1020
+ );
1021
+ Value::Object(map)
1022
+ }
1023
+
1024
+ fn repair_task_projection_null() -> Value {
1025
+ let mut map = serde_json::Map::new();
1026
+ map.insert("assignee".to_string(), Value::Null);
1027
+ map.insert("status".to_string(), Value::Null);
1028
+ map.insert("last_result_summary".to_string(), Value::Null);
1029
+ Value::Object(map)
1030
+ }
1031
+
1032
+ fn load_team_spec_optional(workspace: &Path, state: &Value) -> Result<Option<crate::model::yaml::Value>, CliError> {
1033
+ let spec_path = state
1034
+ .get("spec_path")
1035
+ .and_then(Value::as_str)
1036
+ .filter(|path| !path.is_empty())
1037
+ .map(PathBuf::from)
1038
+ .or_else(|| {
1039
+ state
1040
+ .get("team_dir")
1041
+ .and_then(Value::as_str)
1042
+ .filter(|path| !path.is_empty())
1043
+ .map(|path| PathBuf::from(path).join("team.spec.yaml"))
1044
+ })
1045
+ .unwrap_or_else(|| workspace.join("team.spec.yaml"));
1046
+ if !spec_path.exists() {
1047
+ return Ok(None);
1048
+ }
1049
+ let text = std::fs::read_to_string(&spec_path)?;
1050
+ crate::model::yaml::loads(&text)
1051
+ .map(Some)
1052
+ .map_err(|e| CliError::Runtime(e.to_string()))
1053
+ }
1054
+
1055
+ /// `cmd_approvals`(`commands.py:112`)。
1056
+ pub fn cmd_approvals(args: &ApprovalsArgs) -> Result<CmdResult, CliError> {
1057
+ let value = status_port::approvals(&args.workspace, args.agent.as_deref(), args.json)?;
1058
+ if args.json {
1059
+ Ok(CmdResult::from_json(value, true))
1060
+ } else {
1061
+ Ok(CmdResult::human(status_port::format_approvals(&value)))
1062
+ }
1063
+ }
1064
+
1065
+ /// `cmd_inbox`(`commands.py:137`)。
1066
+ pub fn cmd_inbox(args: &InboxArgs) -> Result<CmdResult, CliError> {
1067
+ let value = status_port::inbox(
1068
+ &args.workspace,
1069
+ &args.agent,
1070
+ args.limit,
1071
+ args.since.as_deref(),
1072
+ args.json,
1073
+ )?;
1074
+ if args.json {
1075
+ Ok(CmdResult::from_json(value, true))
1076
+ } else {
1077
+ Ok(CmdResult::human(format_inbox_human(
1078
+ &args.workspace,
1079
+ &args.agent,
1080
+ args.since.as_deref(),
1081
+ &value,
1082
+ )?))
1083
+ }
1084
+ }
1085
+
1086
+ fn format_inbox_human(
1087
+ workspace: &Path,
1088
+ agent: &str,
1089
+ since: Option<&str>,
1090
+ value: &Value,
1091
+ ) -> Result<String, CliError> {
1092
+ let messages = value
1093
+ .get("messages")
1094
+ .and_then(Value::as_array)
1095
+ .cloned()
1096
+ .unwrap_or_default();
1097
+ let mut lines = Vec::new();
1098
+ if messages.is_empty() {
1099
+ let mut line = format!("{agent}: no messages");
1100
+ if let Some(since) = since {
1101
+ line.push_str(" since ");
1102
+ line.push_str(since);
1103
+ }
1104
+ lines.push(line);
1105
+ } else {
1106
+ lines.push(format!("{agent}: {} message(s)", messages.len()));
1107
+ for message in messages {
1108
+ let sender = message.get("sender").and_then(Value::as_str).unwrap_or("-");
1109
+ let content = message.get("content").and_then(Value::as_str).unwrap_or("");
1110
+ lines.push(format!("- {sender}: {content}"));
1111
+ }
1112
+ }
1113
+ let pending = uncollected_result_count(workspace)?;
1114
+ let mut note = "final results are not in inbox; use team-agent collect".to_string();
1115
+ if pending > 0 {
1116
+ note.push_str(&format!(" ({pending} uncollected result(s) pending)"));
1117
+ }
1118
+ lines.push(note);
1119
+ Ok(lines.join("\n"))
1120
+ }
1121
+
1122
+ fn uncollected_result_count(workspace: &Path) -> Result<i64, CliError> {
1123
+ let store = crate::message_store::MessageStore::open(workspace)
1124
+ .map_err(|e| CliError::Runtime(e.to_string()))?;
1125
+ let conn = crate::db::schema::open_db(store.db_path())
1126
+ .map_err(|e| CliError::Runtime(e.to_string()))?;
1127
+ conn.query_row(
1128
+ "select count(*) from results where status not in ('collected', 'invalid')",
1129
+ [],
1130
+ |row| row.get::<_, i64>(0),
1131
+ )
1132
+ .map_err(|e| CliError::Runtime(e.to_string()))
1133
+ }
1134
+
1135
+ /// `cmd_takeover`(`commands.py:152`)。
1136
+ pub fn cmd_takeover(args: &TakeoverArgs) -> Result<CmdResult, CliError> {
1137
+ Ok(CmdResult::from_json(
1138
+ leader_port::takeover(&args.workspace, args.team.as_deref(), args.confirm)?,
1139
+ args.json,
1140
+ ))
1141
+ }
1142
+
1143
+ /// `cmd_claim_leader`(`commands.py:156`)。
1144
+ pub fn cmd_claim_leader(args: &ClaimLeaderArgs) -> Result<CmdResult, CliError> {
1145
+ Ok(CmdResult::from_json(
1146
+ leader_port::claim_leader(&args.workspace, args.team.as_deref(), args.confirm)?,
1147
+ args.json,
1148
+ ))
1149
+ }
1150
+
1151
+ /// `cmd_identity`(`commands.py:160`)。
1152
+ pub fn cmd_identity(args: &IdentityArgs) -> Result<CmdResult, CliError> {
1153
+ Ok(CmdResult::from_json(
1154
+ leader_port::leader_identity(&args.workspace, args.team.as_deref())?,
1155
+ args.json,
1156
+ ))
1157
+ }
1158
+
1159
+ /// `cmd_shutdown`(`commands.py:340`)。
1160
+ pub fn cmd_shutdown(args: &ShutdownArgs) -> Result<CmdResult, CliError> {
1161
+ Ok(CmdResult::from_json(
1162
+ lifecycle_port::shutdown(&args.workspace, args.keep_logs, args.team.as_deref())?,
1163
+ args.json,
1164
+ ))
1165
+ }
1166
+
1167
+ /// `cmd_restart`(`commands.py:344`)。
1168
+ pub fn cmd_restart(args: &RestartArgs) -> Result<CmdResult, CliError> {
1169
+ Ok(CmdResult::from_json(
1170
+ lifecycle_port::restart(&args.workspace, args.allow_fresh, args.team.as_deref())?,
1171
+ args.json,
1172
+ ))
1173
+ }
1174
+
1175
+ /// `cmd_start_agent`(`commands.py:348`)。
1176
+ pub fn cmd_start_agent(args: &StartAgentArgs) -> Result<CmdResult, CliError> {
1177
+ Ok(CmdResult::from_json(
1178
+ lifecycle_port::start_agent(
1179
+ &args.workspace,
1180
+ &args.agent,
1181
+ args.force,
1182
+ !args.no_display,
1183
+ args.allow_fresh,
1184
+ args.team.as_deref(),
1185
+ )?,
1186
+ args.json,
1187
+ ))
1188
+ }
1189
+
1190
+ /// `cmd_stop_agent`(`commands.py:359`)。
1191
+ pub fn cmd_stop_agent(args: &StopAgentArgs) -> Result<CmdResult, CliError> {
1192
+ Ok(CmdResult::from_json(
1193
+ lifecycle_port::stop_agent(&args.workspace, &args.agent, args.team.as_deref())?,
1194
+ args.json,
1195
+ ))
1196
+ }
1197
+
1198
+ /// `cmd_reset_agent`(`commands.py:363`)。
1199
+ pub fn cmd_reset_agent(args: &ResetAgentArgs) -> Result<CmdResult, CliError> {
1200
+ Ok(CmdResult::from_json(
1201
+ lifecycle_port::reset_agent(
1202
+ &args.workspace,
1203
+ &args.agent,
1204
+ args.discard_session,
1205
+ !args.no_display,
1206
+ args.team.as_deref(),
1207
+ )?,
1208
+ args.json,
1209
+ ))
1210
+ }
1211
+
1212
+ /// `cmd_add_agent`(`commands.py:373`)。
1213
+ pub fn cmd_add_agent(args: &AddAgentArgs) -> Result<CmdResult, CliError> {
1214
+ Ok(CmdResult::from_json(
1215
+ lifecycle_port::add_agent(
1216
+ &args.workspace,
1217
+ &args.agent,
1218
+ &args.role_file,
1219
+ !args.no_display,
1220
+ args.team.as_deref(),
1221
+ )?,
1222
+ args.json,
1223
+ ))
1224
+ }
1225
+
1226
+ /// `cmd_fork_agent`(`commands.py:383`)。
1227
+ pub fn cmd_fork_agent(args: &ForkAgentArgs) -> Result<CmdResult, CliError> {
1228
+ Ok(CmdResult::from_json(
1229
+ lifecycle_port::fork_agent(
1230
+ &args.workspace,
1231
+ &args.source_agent,
1232
+ &args.as_agent,
1233
+ args.label.as_deref(),
1234
+ !args.no_display,
1235
+ args.team.as_deref(),
1236
+ )?,
1237
+ args.json,
1238
+ ))
1239
+ }
1240
+
1241
+ /// `cmd_remove_agent`(`commands.py:394`)。
1242
+ pub fn cmd_remove_agent(args: &RemoveAgentArgs) -> Result<CmdResult, CliError> {
1243
+ Ok(CmdResult::from_json(
1244
+ lifecycle_port::remove_agent(
1245
+ &args.workspace,
1246
+ &args.agent,
1247
+ args.from_spec,
1248
+ args.confirm,
1249
+ args.force,
1250
+ args.team.as_deref(),
1251
+ )?,
1252
+ args.json,
1253
+ ))
1254
+ }
1255
+
1256
+ /// `cmd_stuck_list`(`commands.py:405`)。REUSE `messaging::stuck_list`。
1257
+ pub fn cmd_stuck_list(args: &StuckListArgs) -> Result<CmdResult, CliError> {
1258
+ Ok(CmdResult::from_json(messaging::stuck_list(&args.workspace)?, args.json))
1259
+ }
1260
+
1261
+ /// `cmd_stuck_cancel`(`commands.py:409`)。REUSE `messaging::stuck_cancel`(suppressed_by="leader")。
1262
+ pub fn cmd_stuck_cancel(args: &StuckCancelArgs) -> Result<CmdResult, CliError> {
1263
+ Ok(CmdResult::from_json(
1264
+ messaging::stuck_cancel(&args.workspace, &args.agent, args.alert_type, "leader")?,
1265
+ args.json,
1266
+ ))
1267
+ }
1268
+
1269
+ /// `cmd_acknowledge_idle`(`commands.py:418`)。
1270
+ pub fn cmd_acknowledge_idle(args: &AcknowledgeIdleArgs) -> Result<CmdResult, CliError> {
1271
+ Ok(CmdResult::from_json(
1272
+ lifecycle_port::acknowledge_idle(&args.workspace, args.team.as_deref())?,
1273
+ args.json,
1274
+ ))
1275
+ }
1276
+
1277
+ /// `cmd_doctor`(`commands.py:218`)。分派:`--fix` 缺 gate→Usage err;`--comms`/`gate==comms`→
1278
+ /// `diagnose_port::comms_selftest`(+ COMMS_BOUNDARY_TEXT 人读前缀);`gate==orphans`→`orphan_gate`;
1279
+ /// 非 gate:`--fix-schema`→`fix_schema`;schema drift→注入;`--cleanup-orphans`→`cleanup_orphans`;
1280
+ /// else→`diagnose_port::doctor(spec)` + schema 注入。返回 `dict | str`(comms 人读 = boundary+json)。
1281
+ pub fn cmd_doctor(args: &DoctorArgs) -> Result<CmdResult, CliError> {
1282
+ if args.fix && args.gate.is_none() {
1283
+ return Err(CliError::Runtime("--fix requires --gate".to_string()));
1284
+ }
1285
+ if args.comms || matches!(args.gate, Some(DoctorGate::Comms)) {
1286
+ let value = diagnose_port::comms_selftest(&args.workspace, args.team.as_deref(), Some("comms"))?;
1287
+ if !args.json {
1288
+ let json_tail = serde_json::to_string_pretty(&sort_json(&value))?;
1289
+ return Ok(CmdResult::human(format!("{COMMS_BOUNDARY_TEXT}\n{json_tail}")));
1290
+ }
1291
+ return Ok(CmdResult::from_json(value, true));
1292
+ }
1293
+ let value = if matches!(args.gate, Some(DoctorGate::Orphans)) {
1294
+ diagnose_port::orphan_gate(args.fix, args.confirm)?
1295
+ } else if args.cleanup_orphans {
1296
+ diagnose_port::cleanup_orphans(args.confirm)?
1297
+ } else if args.fix_schema {
1298
+ diagnose_port::fix_schema(&args.workspace)?
1299
+ } else {
1300
+ diagnose_port::doctor(&args.workspace, args.spec.as_deref())?
1301
+ };
1302
+ Ok(CmdResult::from_json(value, args.json))
1303
+ }
1304
+
1305
+ #[cfg(test)]
1306
+ mod tests {
1307
+ #![allow(clippy::unwrap_used)]
1308
+
1309
+ use super::agent_pane_id;
1310
+ use serde_json::json;
1311
+
1312
+ #[test]
1313
+ fn agent_pane_id_resolves_session_window_even_with_recorded_pane_id() {
1314
+ let state = json!({
1315
+ "session_name": "team-x",
1316
+ "agents": {
1317
+ "w1": {"pane_id": "%7", "window": "w1"}
1318
+ }
1319
+ });
1320
+ let agent = state.get("agents").and_then(|agents| agents.get("w1")).unwrap();
1321
+
1322
+ assert_eq!(
1323
+ agent_pane_id(&state, "w1", agent).unwrap(),
1324
+ ("team-x".to_string(), "w1".to_string(), "team-x:w1".to_string())
1325
+ );
1326
+ }
1327
+
1328
+ #[test]
1329
+ fn agent_pane_id_falls_back_to_session_window_target() {
1330
+ let state = json!({
1331
+ "session_name": "team-x",
1332
+ "agents": {
1333
+ "w1": {"window": "worker-one"}
1334
+ }
1335
+ });
1336
+ let agent = state.get("agents").and_then(|agents| agents.get("w1")).unwrap();
1337
+
1338
+ assert_eq!(
1339
+ agent_pane_id(&state, "w1", agent).unwrap(),
1340
+ ("team-x".to_string(), "worker-one".to_string(), "team-x:worker-one".to_string())
1341
+ );
1342
+ }
1343
+ }