@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,1204 @@
1
+ //! cli · emit — `emit`(--json vs 人读 dict 逐键)+ 顶层 `run` 调度(parser.py `main`)+
2
+ //! 人读标量/集合渲染(`human_value` / `json_dumps_like`)。
3
+
4
+ use super::*;
5
+ use std::io::Write as _;
6
+
7
+ /// `emit`(`helpers.py:12-23`):`--json`→`json.dumps(indent=2, ensure_ascii=False, sort_keys=True)`;
8
+ /// 否则 dict 逐键 `key: value`(嵌套 dict/list 内联 compact json,`ensure_ascii=False`)、非 dict 直接 print。
9
+ /// 返回应打印到 stdout 的字符串(bin main 负责实际 println)。
10
+ pub fn emit(output: &CmdOutput, as_json: bool) -> Option<String> {
11
+ match output {
12
+ CmdOutput::None => None,
13
+ CmdOutput::Human(text) => Some(text.clone()),
14
+ CmdOutput::Json(value) if as_json => serde_json::to_string_pretty(&sort_json(value)).ok(),
15
+ CmdOutput::Json(Value::Object(obj)) => {
16
+ let lines: Vec<String> = obj
17
+ .iter()
18
+ .map(|(key, value)| format!("{key}: {}", human_value(value)))
19
+ .collect();
20
+ Some(lines.join("\n"))
21
+ }
22
+ CmdOutput::Json(value) => Some(human_value(value)),
23
+ }
24
+ }
25
+
26
+ /// `main(argv)`(`parser.py:84`):**CLI 唯一进程入口**。codex/claude passthrough 早返回 →
27
+ /// 解析 argv 到 subcommand → 调对应 handler → 异常落盘 + 信封 + `ExitCode::Error` →
28
+ /// `consume_leader_inbox_summary` → `emit` → `result.ok is False ? Error : Ok`。
29
+ /// **行为入口**:契约可端到端跑 argv→(stdout, exit code)。
30
+ pub fn run(argv: &[String], cwd: &Path) -> ExitCode {
31
+ let Some(command) = argv.first().map(String::as_str) else {
32
+ return emit_missing_subcommand_usage();
33
+ };
34
+ if command == "codex" || command == "claude" {
35
+ return cmd_leader_passthrough(command, &argv[1..], cwd)
36
+ .map(emit_result)
37
+ .unwrap_or(ExitCode::Error);
38
+ }
39
+ if matches!(command, "-h" | "--help" | "help") {
40
+ println!("{}", command_help(None));
41
+ return ExitCode::Ok;
42
+ }
43
+ // CR-063/G4: every REGISTERED subcommand's `--help` must short-circuit BEFORE dispatch —
44
+ // before argument validation, leader-pane checks, or runtime-state writes. The prior
45
+ // `command_has_help` whitelist (quick-start/start/stop/...) silently dropped `--help`
46
+ // for add-agent / stop-agent / reset-agent / claim-leader / attach-leader, so those
47
+ // fell into handlers that emitted "missing agent" or "caller_not_leader_shaped" and
48
+ // created `.team/logs/cli-error-*.log` under cwd.
49
+ //
50
+ // The gate stays on KNOWN subcommands so an unknown command still falls through to
51
+ // the argparse-style invalid-choice path (golden parser.py:84; covered by
52
+ // `cli_unknown_command_red` and the `claude_code` divergence guard which would
53
+ // otherwise be silently passthrough-shaped).
54
+ if is_known_subcommand(command)
55
+ && argv.iter().skip(1).any(|arg| matches!(arg.as_str(), "-h" | "--help"))
56
+ {
57
+ println!("{}", command_help(Some(command)));
58
+ return ExitCode::Ok;
59
+ }
60
+ match dispatch(command, &argv[1..], cwd) {
61
+ Ok(exit) => exit,
62
+ Err(error) => emit_cli_error(command, &argv[1..], cwd, &error),
63
+ }
64
+ }
65
+
66
+ /// Print a handler's CmdResult to stdout (emit formats json/human), then surface its exit code.
67
+ /// (parser.py: `print(emit(result, as_json))` then the ok→exit mapping.)
68
+ fn emit_result(r: CmdResult) -> ExitCode {
69
+ if let Some(text) = emit(&r.output, r.as_json) {
70
+ println!("{text}");
71
+ }
72
+ r.exit
73
+ }
74
+
75
+ fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliError> {
76
+ match command {
77
+ "init" => cmd_init(&init_args(args, cwd)).map(emit_result),
78
+ "quick-start" => cmd_quick_start(&quick_start_args(args, cwd)?).map(emit_result),
79
+ "compile" => cmd_compile(&compile_args(args, cwd)?).map(emit_result),
80
+ "send" => cmd_send(&send_args(args, cwd)?).map(emit_result),
81
+ "allow-peer-talk" => cmd_allow_peer_talk(&allow_peer_talk_args(args, cwd)?).map(emit_result),
82
+ "status" => cmd_status(&status_args(args, cwd)).map(emit_result),
83
+ "stop" => cmd_shutdown(&shutdown_args(args, cwd)).map(emit_result),
84
+ "shutdown" => cmd_shutdown(&shutdown_args(args, cwd)).map(emit_result),
85
+ "restart" => cmd_restart(&restart_args(args, cwd)).map(emit_result),
86
+ "restart-agent" => cmd_reset_agent(&reset_agent_args(args, cwd)?).map(emit_result),
87
+ "start-agent" => cmd_start_agent(&start_agent_args(args, cwd)?).map(emit_result),
88
+ "stop-agent" => cmd_stop_agent(&stop_agent_args(args, cwd)?).map(emit_result),
89
+ "reset-agent" => cmd_reset_agent(&reset_agent_args(args, cwd)?).map(emit_result),
90
+ "add-agent" => cmd_add_agent(&add_agent_args(args, cwd)?).map(emit_result),
91
+ "fork-agent" => cmd_fork_agent(&fork_agent_args(args, cwd)?).map(emit_result),
92
+ "remove-agent" => cmd_remove_agent(&remove_agent_args(args, cwd)?).map(emit_result),
93
+ "stuck-list" => cmd_stuck_list(&stuck_list_args(args, cwd)).map(emit_result),
94
+ "stuck-cancel" => cmd_stuck_cancel(&stuck_cancel_args(args, cwd)?).map(emit_result),
95
+ "acknowledge-idle" => cmd_acknowledge_idle(&acknowledge_idle_args(args, cwd)).map(emit_result),
96
+ "takeover" => cmd_takeover(&takeover_args(args, cwd)).map(emit_result),
97
+ "claim-leader" => cmd_claim_leader(&claim_leader_args(args, cwd)).map(emit_result),
98
+ "identity" => cmd_identity(&identity_args(args, cwd)).map(emit_result),
99
+ "approvals" => cmd_approvals(&approvals_args(args, cwd)).map(emit_result),
100
+ "inbox" => cmd_inbox(&inbox_args(args, cwd)?).map(emit_result),
101
+ "doctor" => cmd_doctor(&doctor_args(args, cwd)).map(emit_result),
102
+ "watch" => cmd_watch(&watch_args(args, cwd)).map(emit_result),
103
+ "sessions" => cmd_sessions(&sessions_args(args, cwd)).map(emit_result),
104
+ "validate" => cmd_validate(&validate_args(args, cwd)).map(emit_result),
105
+ "profile" => cmd_profile(&profile_args(args, cwd)?).map(emit_result),
106
+ "validate-result" if has_arg(args, "--result") => {
107
+ eprintln!("team-agent: error: unrecognized arguments: --result");
108
+ Ok(ExitCode::Usage)
109
+ }
110
+ "validate-result" => cmd_validate_result(&validate_result_args(args)?).map(emit_result),
111
+ "collect" => cmd_collect(&collect_args(args, cwd)).map(emit_result),
112
+ "settle" => cmd_settle(&settle_args(args, cwd)).map(emit_result),
113
+ "repair-state" => cmd_repair_state(&repair_state_args(args, cwd)?).map(emit_result),
114
+ "diagnose" => cmd_diagnose(&diagnose_args(args, cwd)).map(emit_result),
115
+ "preflight" => cmd_preflight(&preflight_args(args, cwd)).map(emit_result),
116
+ "wait-ready" => cmd_wait_ready(&wait_ready_args(args, cwd)).map(emit_result),
117
+ "e2e" => cmd_e2e(&e2e_args(args, cwd)).map(emit_result),
118
+ "peek" => cmd_peek(&peek_args(args, cwd)?).map(emit_result),
119
+ "coordinator" => run_coordinator(args, cwd),
120
+ _ => Ok(emit_unknown_subcommand_usage(command)),
121
+ }
122
+ }
123
+
124
+ const DISPATCH_COMMANDS: &[&str] = &[
125
+ "init",
126
+ "quick-start",
127
+ "compile",
128
+ "send",
129
+ "allow-peer-talk",
130
+ "status",
131
+ "stop",
132
+ "shutdown",
133
+ "restart",
134
+ "restart-agent",
135
+ "start-agent",
136
+ "stop-agent",
137
+ "reset-agent",
138
+ "add-agent",
139
+ "fork-agent",
140
+ "remove-agent",
141
+ "stuck-list",
142
+ "stuck-cancel",
143
+ "acknowledge-idle",
144
+ "takeover",
145
+ "claim-leader",
146
+ "identity",
147
+ "approvals",
148
+ "inbox",
149
+ "doctor",
150
+ "watch",
151
+ "sessions",
152
+ "validate",
153
+ "profile",
154
+ "validate-result",
155
+ "collect",
156
+ "settle",
157
+ "repair-state",
158
+ "diagnose",
159
+ "preflight",
160
+ "wait-ready",
161
+ "e2e",
162
+ "peek",
163
+ "coordinator",
164
+ ];
165
+
166
+ const SPEC_ONLY_HELP_COMMANDS: &[&str] = &["start", "purge-agent", "attach-leader"];
167
+
168
+ fn emit_missing_subcommand_usage() -> ExitCode {
169
+ emit_usage_error("the following arguments are required: {codex,claude,...,doctor}");
170
+ ExitCode::Usage
171
+ }
172
+
173
+ /// Registered subcommands (the dispatch table) PLUS spec-only verbs that have no
174
+ /// dispatch arm yet but must still respond to `--help` per CR-063/G4 (`attach-leader`).
175
+ /// Used by the `--help` short-circuit gate so unknown commands keep falling through
176
+ /// to the argparse invalid-choice path.
177
+ fn is_known_subcommand(command: &str) -> bool {
178
+ DISPATCH_COMMANDS.contains(&command) || SPEC_ONLY_HELP_COMMANDS.contains(&command)
179
+ }
180
+
181
+ fn command_help(command: Option<&str>) -> String {
182
+ match command {
183
+ None => {
184
+ let mut commands = vec!["codex", "claude"];
185
+ commands.extend_from_slice(DISPATCH_COMMANDS);
186
+ commands.extend_from_slice(SPEC_ONLY_HELP_COMMANDS);
187
+ format!(
188
+ "usage: team-agent <command> [options]\n\nCommands: {}\n\nRun `team-agent <command> --help` for command flags.",
189
+ commands.join(", ")
190
+ )
191
+ }
192
+ Some("init") => "usage: team-agent init [--workspace WORKSPACE] [--force] [--json]".to_string(),
193
+ Some("quick-start") => "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--fresh] [--json]".to_string(),
194
+ Some("start") => "usage: team-agent start [TEAMDIR] [--yes] [--fresh] [--json]".to_string(),
195
+ Some("compile") => "usage: team-agent compile --team TEAM [--out FILE] [--json]".to_string(),
196
+ Some("send") => "usage: team-agent send TARGET MESSAGE... [--workspace WORKSPACE] [--team TEAM] [--targets AGENTS] [--task TASK] [--sender SENDER] [--watch-result] [--requires-ack|--no-ack] [--no-wait] [--timeout SECONDS] [--confirm-human] [--message-id ID] [--json]".to_string(),
197
+ Some("allow-peer-talk") => "usage: team-agent allow-peer-talk A B [--workspace WORKSPACE] [--json]".to_string(),
198
+ Some("status") => "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--summary|--json] [--detail]".to_string(),
199
+ Some("stop") => "usage: team-agent stop [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]".to_string(),
200
+ Some("shutdown") => "usage: team-agent shutdown [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]".to_string(),
201
+ Some("restart") => "usage: team-agent restart [WORKSPACE] [--team TEAM] [--allow-fresh] [--json]".to_string(),
202
+ Some("restart-agent") => "usage: team-agent restart-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--discard-session] [--no-display] [--json]".to_string(),
203
+ Some("reset-agent") => "usage: team-agent reset-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--discard-session] [--no-display] [--json]".to_string(),
204
+ Some("start-agent") => "usage: team-agent start-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--force] [--allow-fresh] [--no-display] [--json]".to_string(),
205
+ Some("stop-agent") => "usage: team-agent stop-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
206
+ Some("add-agent") => "usage: team-agent add-agent AGENT --role-file FILE [--workspace WORKSPACE] [--team TEAM] [--no-display] [--json]".to_string(),
207
+ Some("fork-agent") => "usage: team-agent fork-agent SOURCE_AGENT --as AGENT [--label LABEL] [--workspace WORKSPACE] [--team TEAM] [--no-display] [--json]".to_string(),
208
+ Some("remove-agent") => "usage: team-agent remove-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--from-spec] [--confirm] [--force] [--json]".to_string(),
209
+ Some("purge-agent") => "usage: team-agent purge-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--force] [--json]".to_string(),
210
+ Some("stuck-list") => "usage: team-agent stuck-list [--workspace WORKSPACE] [--json]".to_string(),
211
+ Some("stuck-cancel") => "usage: team-agent stuck-cancel AGENT [--workspace WORKSPACE] [--alert-type stuck|idle_fallback|cross_worker_deadlock|all] [--json]".to_string(),
212
+ Some("acknowledge-idle") => "usage: team-agent acknowledge-idle [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
213
+ Some("takeover") => "usage: team-agent takeover [--workspace WORKSPACE] [--team TEAM] [--confirm] [--json]".to_string(),
214
+ Some("claim-leader") => "usage: team-agent claim-leader [--workspace WORKSPACE] [--team TEAM] [--confirm] [--json]".to_string(),
215
+ Some("attach-leader") => "usage: team-agent attach-leader [--workspace WORKSPACE] [--team TEAM] [--confirm] [--json]".to_string(),
216
+ Some("identity") => "usage: team-agent identity [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
217
+ Some("approvals") => "usage: team-agent approvals [AGENT] [--workspace WORKSPACE] [--json]".to_string(),
218
+ Some("inbox") => "usage: team-agent inbox AGENT [--workspace WORKSPACE] [--limit N] [--since CURSOR] [--json]".to_string(),
219
+ Some("doctor") => "usage: team-agent doctor [SPEC] [--workspace WORKSPACE] [--team TEAM] [--gate orphans|comms] [--comms] [--fix] [--fix-schema] [--cleanup-orphans] [--confirm] [--json]".to_string(),
220
+ Some("watch") => "usage: team-agent watch [--workspace WORKSPACE] [--team TEAM]".to_string(),
221
+ Some("sessions") => "usage: team-agent sessions [--workspace WORKSPACE] [--json]".to_string(),
222
+ Some("validate") => "usage: team-agent validate [SPEC] [--json]".to_string(),
223
+ Some("profile") => "usage: team-agent profile COMMAND NAME [--workspace WORKSPACE] [--team TEAM] [--auth-mode MODE] [--json]".to_string(),
224
+ Some("validate-result") => "usage: team-agent validate-result [ENVELOPE] [--file FILE|--result JSON] [--json]".to_string(),
225
+ Some("collect") => "usage: team-agent collect [--workspace WORKSPACE] [--result-file FILE] [--json]".to_string(),
226
+ Some("settle") => "usage: team-agent settle [--workspace WORKSPACE] [--json]".to_string(),
227
+ Some("repair-state") => "usage: team-agent repair-state --task TASK --status STATUS [SUMMARY] [--assignee AGENT] [--workspace WORKSPACE] [--json]".to_string(),
228
+ Some("diagnose") => "usage: team-agent diagnose [--workspace WORKSPACE] [--json]".to_string(),
229
+ Some("preflight") => "usage: team-agent preflight [TEAMDIR] [--json]".to_string(),
230
+ Some("wait-ready") => "usage: team-agent wait-ready [--workspace WORKSPACE] [--timeout SECONDS] [--json]".to_string(),
231
+ Some("e2e") => "usage: team-agent e2e [--workspace WORKSPACE] [--providers LIST] [--real] [--json]".to_string(),
232
+ Some("peek") => "usage: team-agent peek AGENT [--workspace WORKSPACE] [--tail N] [--allow-raw-screen] [--json]".to_string(),
233
+ Some("coordinator") => "usage: team-agent coordinator [--workspace WORKSPACE] [--once] [--tick-interval SECONDS]".to_string(),
234
+ Some(other) => format!("usage: team-agent {other} [options]"),
235
+ }
236
+ }
237
+
238
+ fn emit_unknown_subcommand_usage(command: &str) -> ExitCode {
239
+ emit_usage_error(&format!(
240
+ "argument {{codex,claude,...,doctor}}: invalid choice: '{command}' (choose from codex, claude, ..., doctor)"
241
+ ));
242
+ ExitCode::Usage
243
+ }
244
+
245
+ fn emit_usage_error(message: &str) {
246
+ eprintln!("usage: team-agent [-h] {{codex,claude,...,doctor}} ...");
247
+ eprintln!("team-agent: error: {message}");
248
+ }
249
+
250
+ /// `cmd_validate` delegates to runtime validate_file.
251
+ pub fn cmd_validate(args: &ValidateArgs) -> Result<CmdResult, CliError> {
252
+ let spec = resolve_path(&args.spec);
253
+ let value = if spec.is_dir() {
254
+ validate_team_dir(&spec)?
255
+ } else {
256
+ validate_spec_file(&spec)?
257
+ };
258
+ Ok(CmdResult::from_json(value, args.json))
259
+ }
260
+
261
+ fn validate_spec_file(spec_path: &Path) -> Result<Value, CliError> {
262
+ let text = std::fs::read_to_string(spec_path)?;
263
+ let base_dir = spec_path.parent().unwrap_or_else(|| Path::new("."));
264
+ let spec = crate::model::spec::load_and_validate_spec(&text, base_dir)
265
+ .map_err(model_error_to_cli)?;
266
+ let team = spec
267
+ .get("team")
268
+ .and_then(|team| team.get("name"))
269
+ .and_then(crate::model::yaml::Value::as_str)
270
+ .unwrap_or("");
271
+ let workspace = spec
272
+ .get("team")
273
+ .and_then(|team| team.get("workspace"))
274
+ .and_then(crate::model::yaml::Value::as_str)
275
+ .map(PathBuf::from)
276
+ .unwrap_or_else(|| base_dir.to_path_buf());
277
+ let mut obj = Map::new();
278
+ obj.insert("ok".to_string(), Value::Bool(true));
279
+ obj.insert("team".to_string(), Value::String(team.to_string()));
280
+ obj.insert(
281
+ "workspace".to_string(),
282
+ Value::String(workspace.to_string_lossy().to_string()),
283
+ );
284
+ Ok(Value::Object(obj))
285
+ }
286
+
287
+ fn validate_team_dir(team_dir: &Path) -> Result<Value, CliError> {
288
+ let spec = crate::compiler::compile_team(team_dir).map_err(model_error_to_cli)?;
289
+ let team = spec
290
+ .get("team")
291
+ .and_then(|team| team.get("name"))
292
+ .and_then(crate::model::yaml::Value::as_str)
293
+ .unwrap_or("");
294
+ let workspace = spec
295
+ .get("team")
296
+ .and_then(|team| team.get("workspace"))
297
+ .and_then(crate::model::yaml::Value::as_str)
298
+ .map(PathBuf::from)
299
+ .unwrap_or_else(|| team_dir.to_path_buf());
300
+ let agents = spec
301
+ .get("agents")
302
+ .and_then(crate::model::yaml::Value::as_list)
303
+ .map(|agents| {
304
+ agents
305
+ .iter()
306
+ .filter_map(|agent| {
307
+ agent
308
+ .get("id")
309
+ .and_then(crate::model::yaml::Value::as_str)
310
+ .map(|id| Value::String(id.to_string()))
311
+ })
312
+ .collect::<Vec<_>>()
313
+ })
314
+ .unwrap_or_default();
315
+ let mut obj = Map::new();
316
+ obj.insert("ok".to_string(), Value::Bool(true));
317
+ obj.insert("type".to_string(), Value::String("team_dir".to_string()));
318
+ obj.insert(
319
+ "workspace".to_string(),
320
+ Value::String(workspace.to_string_lossy().to_string()),
321
+ );
322
+ obj.insert("team".to_string(), Value::String(team.to_string()));
323
+ obj.insert("agents".to_string(), Value::Array(agents));
324
+ Ok(Value::Object(obj))
325
+ }
326
+
327
+ fn resolve_path(path: &Path) -> PathBuf {
328
+ std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
329
+ }
330
+
331
+ fn model_error_to_cli(error: crate::model::errors::ModelError) -> CliError {
332
+ match error {
333
+ crate::model::errors::ModelError::Validation(message) => CliError::Runtime(message),
334
+ other => CliError::Runtime(other.to_string()),
335
+ }
336
+ }
337
+
338
+ fn emit_cli_error(command: &str, args: &[String], cwd: &Path, error: &CliError) -> ExitCode {
339
+ let parsed = parse_args(args);
340
+ let workspace = workspace(&parsed, cwd);
341
+ let log_path = cli_error_log_path(&workspace);
342
+ if let Some(parent) = log_path.parent() {
343
+ let _ = std::fs::create_dir_all(parent);
344
+ }
345
+ let normalized = normalize_cli_error(error);
346
+ let payload_error = normalized.as_ref().unwrap_or(error);
347
+ let _ = std::fs::write(&log_path, format!("{payload_error}\n"));
348
+ let payload = payload_error.to_payload(&log_path, command);
349
+ if has_arg(args, "--json") {
350
+ if let Ok(value) = serde_json::to_value(payload) {
351
+ println!("{}", python_compact_json(&value));
352
+ }
353
+ } else {
354
+ eprintln!("error: {}", payload.error);
355
+ eprintln!("action: {}", payload.action);
356
+ eprintln!("log: {}", payload.log);
357
+ }
358
+ ExitCode::Error
359
+ }
360
+
361
+ fn normalize_cli_error(error: &CliError) -> Option<CliError> {
362
+ match error {
363
+ CliError::Runtime(message) => Some(CliError::Runtime(
364
+ message
365
+ .strip_prefix("validation error: ")
366
+ .unwrap_or(message)
367
+ .to_string(),
368
+ )),
369
+ _ => None,
370
+ }
371
+ }
372
+
373
+ fn cli_error_log_path(workspace: &Path) -> PathBuf {
374
+ let stamp = chrono::Utc::now().format("%Y%m%d-%H%M%S%.6f");
375
+ workspace
376
+ .join(".team")
377
+ .join("logs")
378
+ .join(format!("cli-error-{stamp}.log"))
379
+ }
380
+
381
+ struct PythonCompactFormatter;
382
+
383
+ impl serde_json::ser::Formatter for PythonCompactFormatter {
384
+ fn begin_array_value<W: ?Sized + std::io::Write>(&mut self, w: &mut W, first: bool) -> std::io::Result<()> {
385
+ if first { Ok(()) } else { w.write_all(b", ") }
386
+ }
387
+
388
+ fn begin_object_key<W: ?Sized + std::io::Write>(&mut self, w: &mut W, first: bool) -> std::io::Result<()> {
389
+ if first { Ok(()) } else { w.write_all(b", ") }
390
+ }
391
+
392
+ fn begin_object_value<W: ?Sized + std::io::Write>(&mut self, w: &mut W) -> std::io::Result<()> {
393
+ w.write_all(b": ")
394
+ }
395
+ }
396
+
397
+ fn python_compact_json(value: &Value) -> String {
398
+ let mut bytes = Vec::new();
399
+ let mut ser = serde_json::Serializer::with_formatter(&mut bytes, PythonCompactFormatter);
400
+ if value.serialize(&mut ser).is_err() {
401
+ return "{}".to_string();
402
+ }
403
+ String::from_utf8(bytes).unwrap_or_else(|_| "{}".to_string())
404
+ }
405
+
406
+ fn has_arg(args: &[String], needle: &str) -> bool {
407
+ args.iter().any(|arg| arg == needle)
408
+ }
409
+
410
+ #[derive(Debug, Default)]
411
+ struct ParsedArgs {
412
+ positionals: Vec<String>,
413
+ workspace: Option<PathBuf>,
414
+ team: Option<String>,
415
+ json: bool,
416
+ yes: bool,
417
+ fresh: bool,
418
+ name: Option<String>,
419
+ team_id: Option<String>,
420
+ targets: Option<String>,
421
+ task: Option<String>,
422
+ sender: Option<String>,
423
+ watch_result: bool,
424
+ requires_ack: bool,
425
+ no_ack: bool,
426
+ no_wait: bool,
427
+ timeout: Option<f64>,
428
+ confirm_human: bool,
429
+ detail: bool,
430
+ summary: bool,
431
+ keep_logs: bool,
432
+ allow_fresh: bool,
433
+ force: bool,
434
+ no_display: bool,
435
+ discard_session: bool,
436
+ role_file: Option<String>,
437
+ as_agent: Option<String>,
438
+ label: Option<String>,
439
+ from_spec: bool,
440
+ confirm: bool,
441
+ alert_type: Option<String>,
442
+ limit: Option<usize>,
443
+ since: Option<String>,
444
+ gate: Option<String>,
445
+ comms: bool,
446
+ fix: bool,
447
+ fix_schema: bool,
448
+ cleanup_orphans: bool,
449
+ once: bool,
450
+ tick_interval: Option<f64>,
451
+ status_value: Option<String>,
452
+ providers: Option<String>,
453
+ allow_raw_screen: bool,
454
+ tail: Option<usize>,
455
+ result_file: Option<PathBuf>,
456
+ file: Option<PathBuf>,
457
+ result: Option<String>,
458
+ real: bool,
459
+ assignee: Option<String>,
460
+ out: Option<PathBuf>,
461
+ auth_mode: Option<String>,
462
+ message_id: Option<String>,
463
+ }
464
+
465
+ fn parse_args(args: &[String]) -> ParsedArgs {
466
+ let mut parsed = ParsedArgs::default();
467
+ let mut i = 0usize;
468
+ while i < args.len() {
469
+ let Some(arg) = args.get(i) else {
470
+ break;
471
+ };
472
+ match arg.as_str() {
473
+ "--workspace" => {
474
+ parsed.workspace = next_arg(args, &mut i).map(PathBuf::from);
475
+ }
476
+ "--team" => parsed.team = next_arg(args, &mut i),
477
+ "--json" => parsed.json = true,
478
+ "--yes" => parsed.yes = true,
479
+ "--fresh" => parsed.fresh = true,
480
+ "--name" => parsed.name = next_arg(args, &mut i),
481
+ "--team-id" => parsed.team_id = next_arg(args, &mut i),
482
+ "--targets" | "--target" | "--to" => parsed.targets = next_arg(args, &mut i),
483
+ "--task" => parsed.task = next_arg(args, &mut i),
484
+ "--sender" => parsed.sender = next_arg(args, &mut i),
485
+ "--watch-result" => parsed.watch_result = true,
486
+ "--requires-ack" => parsed.requires_ack = true,
487
+ "--no-ack" => parsed.no_ack = true,
488
+ "--no-wait" => parsed.no_wait = true,
489
+ "--timeout" => parsed.timeout = next_arg(args, &mut i).and_then(|v| v.parse::<f64>().ok()),
490
+ "--confirm-human" => parsed.confirm_human = true,
491
+ "--detail" => parsed.detail = true,
492
+ "--summary" => parsed.summary = true,
493
+ "--keep-logs" => parsed.keep_logs = true,
494
+ "--allow-fresh" => parsed.allow_fresh = true,
495
+ "--force" => parsed.force = true,
496
+ "--no-display" => parsed.no_display = true,
497
+ "--discard-session" => parsed.discard_session = true,
498
+ "--role-file" => parsed.role_file = next_arg(args, &mut i),
499
+ "--as" => parsed.as_agent = next_arg(args, &mut i),
500
+ "--label" => parsed.label = next_arg(args, &mut i),
501
+ "--from-spec" => parsed.from_spec = true,
502
+ "--confirm" => parsed.confirm = true,
503
+ "--alert-type" => parsed.alert_type = next_arg(args, &mut i),
504
+ "--limit" => parsed.limit = next_arg(args, &mut i).and_then(|v| v.parse::<usize>().ok()),
505
+ "--since" => parsed.since = next_arg(args, &mut i),
506
+ "--gate" => parsed.gate = next_arg(args, &mut i),
507
+ "--comms" => parsed.comms = true,
508
+ "--fix" => parsed.fix = true,
509
+ "--fix-schema" => parsed.fix_schema = true,
510
+ "--cleanup-orphans" => parsed.cleanup_orphans = true,
511
+ "--once" => parsed.once = true,
512
+ "--tick-interval" => parsed.tick_interval = next_arg(args, &mut i).and_then(|v| v.parse::<f64>().ok()),
513
+ "--status" => parsed.status_value = next_arg(args, &mut i),
514
+ "--providers" => parsed.providers = next_arg(args, &mut i),
515
+ "--allow-raw-screen" => parsed.allow_raw_screen = true,
516
+ "--tail" => parsed.tail = next_arg(args, &mut i).and_then(|v| v.parse::<usize>().ok()),
517
+ "--result-file" => parsed.result_file = next_arg(args, &mut i).map(PathBuf::from),
518
+ "--file" => parsed.file = next_arg(args, &mut i).map(PathBuf::from),
519
+ "--result" => parsed.result = next_arg(args, &mut i),
520
+ "--real" => parsed.real = true,
521
+ "--assignee" => parsed.assignee = next_arg(args, &mut i),
522
+ "--out" => parsed.out = next_arg(args, &mut i).map(PathBuf::from),
523
+ "--auth-mode" => parsed.auth_mode = next_arg(args, &mut i),
524
+ "--message-id" => parsed.message_id = next_arg(args, &mut i),
525
+ "-h" | "--help" => {}
526
+ other if other.starts_with("--team=") => {
527
+ parsed.team = Some(other.trim_start_matches("--team=").to_string());
528
+ }
529
+ other if other.starts_with('-') => {}
530
+ other => parsed.positionals.push(other.to_string()),
531
+ }
532
+ i = i.saturating_add(1);
533
+ }
534
+ parsed
535
+ }
536
+
537
+ fn next_arg(args: &[String], index: &mut usize) -> Option<String> {
538
+ *index = index.saturating_add(1);
539
+ args.get(*index).cloned()
540
+ }
541
+
542
+ fn workspace(parsed: &ParsedArgs, cwd: &Path) -> PathBuf {
543
+ parsed.workspace.clone().unwrap_or_else(|| cwd.to_path_buf())
544
+ }
545
+
546
+ fn required_pos(parsed: &ParsedArgs, index: usize, name: &str) -> Result<String, CliError> {
547
+ parsed
548
+ .positionals
549
+ .get(index)
550
+ .cloned()
551
+ .ok_or_else(|| CliError::Usage(format!("missing {name}")))
552
+ }
553
+
554
+ fn quick_start_args(args: &[String], cwd: &Path) -> Result<QuickStartArgs, CliError> {
555
+ let parsed = parse_args(args);
556
+ let workspace = workspace(&parsed, cwd);
557
+ let agents_dir = parsed
558
+ .positionals
559
+ .first()
560
+ .map(PathBuf::from)
561
+ .unwrap_or_else(|| workspace.clone());
562
+ let agents_dir = if agents_dir.is_absolute() {
563
+ agents_dir
564
+ } else {
565
+ workspace.join(agents_dir)
566
+ };
567
+ Ok(QuickStartArgs {
568
+ agents_dir,
569
+ name: parsed.name,
570
+ team_id: parsed.team_id.or(parsed.team),
571
+ yes: parsed.yes,
572
+ fresh: parsed.fresh,
573
+ json: parsed.json,
574
+ })
575
+ }
576
+
577
+ fn init_args(args: &[String], cwd: &Path) -> InitArgs {
578
+ let parsed = parse_args(args);
579
+ InitArgs {
580
+ workspace: workspace(&parsed, cwd),
581
+ force: parsed.force,
582
+ json: parsed.json,
583
+ }
584
+ }
585
+
586
+ fn compile_args(args: &[String], cwd: &Path) -> Result<CompileArgs, CliError> {
587
+ let parsed = parse_args(args);
588
+ let team = parsed
589
+ .team
590
+ .as_deref()
591
+ .map(PathBuf::from)
592
+ .ok_or_else(|| CliError::Usage("missing --team".to_string()))?;
593
+ let out = parsed.out.unwrap_or_else(|| PathBuf::from("team.spec.yaml"));
594
+ Ok(CompileArgs {
595
+ team: resolve_cli_path(cwd, &team),
596
+ out: resolve_cli_path(cwd, &out),
597
+ json: parsed.json,
598
+ })
599
+ }
600
+
601
+ fn resolve_cli_path(cwd: &Path, path: &Path) -> PathBuf {
602
+ if path.is_absolute() {
603
+ path.to_path_buf()
604
+ } else {
605
+ cwd.join(path)
606
+ }
607
+ }
608
+
609
+ fn send_args(args: &[String], cwd: &Path) -> Result<SendArgs, CliError> {
610
+ let parsed = parse_args(args);
611
+ let target = if parsed.targets.is_some() {
612
+ None
613
+ } else {
614
+ parsed.positionals.first().cloned()
615
+ };
616
+ let message_start = usize::from(target.is_some());
617
+ let workspace = workspace(&parsed, cwd);
618
+ Ok(SendArgs {
619
+ target,
620
+ message: parsed.positionals.iter().skip(message_start).cloned().collect(),
621
+ targets: parsed.targets,
622
+ workspace,
623
+ team: parsed.team,
624
+ task: parsed.task,
625
+ sender: parsed.sender.unwrap_or_else(|| "leader".to_string()),
626
+ no_ack: parsed.no_ack && !parsed.requires_ack,
627
+ no_wait: parsed.no_wait,
628
+ watch_result: parsed.watch_result,
629
+ timeout: parsed.timeout.unwrap_or(30.0),
630
+ confirm_human: parsed.confirm_human,
631
+ json: parsed.json,
632
+ message_id: parsed.message_id,
633
+ })
634
+ }
635
+
636
+ fn allow_peer_talk_args(args: &[String], cwd: &Path) -> Result<AllowPeerTalkArgs, CliError> {
637
+ let parsed = parse_args(args);
638
+ Ok(AllowPeerTalkArgs {
639
+ a: required_pos(&parsed, 0, "a")?,
640
+ b: required_pos(&parsed, 1, "b")?,
641
+ workspace: workspace(&parsed, cwd),
642
+ json: parsed.json,
643
+ })
644
+ }
645
+
646
+ fn status_args(args: &[String], cwd: &Path) -> StatusArgs {
647
+ let parsed = parse_args(args);
648
+ StatusArgs {
649
+ agent: parsed.positionals.first().cloned(),
650
+ workspace: workspace(&parsed, cwd),
651
+ detail: parsed.detail,
652
+ summary: parsed.summary,
653
+ json: parsed.json,
654
+ }
655
+ }
656
+
657
+ fn watch_args(args: &[String], cwd: &Path) -> WatchArgs {
658
+ let parsed = parse_args(args);
659
+ WatchArgs {
660
+ workspace: workspace(&parsed, cwd),
661
+ team: parsed.team,
662
+ }
663
+ }
664
+
665
+ fn approvals_args(args: &[String], cwd: &Path) -> ApprovalsArgs {
666
+ let parsed = parse_args(args);
667
+ ApprovalsArgs {
668
+ agent: parsed.positionals.first().cloned(),
669
+ workspace: workspace(&parsed, cwd),
670
+ json: parsed.json,
671
+ }
672
+ }
673
+
674
+ fn inbox_args(args: &[String], cwd: &Path) -> Result<InboxArgs, CliError> {
675
+ let parsed = parse_args(args);
676
+ Ok(InboxArgs {
677
+ agent: required_pos(&parsed, 0, "agent")?,
678
+ workspace: workspace(&parsed, cwd),
679
+ limit: parsed.limit.unwrap_or(20),
680
+ since: parsed.since,
681
+ json: parsed.json,
682
+ })
683
+ }
684
+
685
+ fn takeover_args(args: &[String], cwd: &Path) -> TakeoverArgs {
686
+ let parsed = parse_args(args);
687
+ TakeoverArgs {
688
+ workspace: workspace(&parsed, cwd),
689
+ team: parsed.team,
690
+ confirm: parsed.confirm,
691
+ json: parsed.json,
692
+ }
693
+ }
694
+
695
+ fn claim_leader_args(args: &[String], cwd: &Path) -> ClaimLeaderArgs {
696
+ let parsed = parse_args(args);
697
+ ClaimLeaderArgs {
698
+ workspace: workspace(&parsed, cwd),
699
+ team: parsed.team,
700
+ confirm: parsed.confirm,
701
+ json: parsed.json,
702
+ }
703
+ }
704
+
705
+ fn identity_args(args: &[String], cwd: &Path) -> IdentityArgs {
706
+ let parsed = parse_args(args);
707
+ IdentityArgs {
708
+ workspace: workspace(&parsed, cwd),
709
+ team: parsed.team,
710
+ json: parsed.json,
711
+ }
712
+ }
713
+
714
+ fn shutdown_args(args: &[String], cwd: &Path) -> ShutdownArgs {
715
+ let parsed = parse_args(args);
716
+ ShutdownArgs {
717
+ workspace: workspace(&parsed, cwd),
718
+ team: parsed.team,
719
+ keep_logs: parsed.keep_logs,
720
+ json: parsed.json,
721
+ }
722
+ }
723
+
724
+ fn restart_args(args: &[String], cwd: &Path) -> RestartArgs {
725
+ let parsed = parse_args(args);
726
+ RestartArgs {
727
+ workspace: parsed
728
+ .positionals
729
+ .first()
730
+ .map(PathBuf::from)
731
+ .unwrap_or_else(|| workspace(&parsed, cwd)),
732
+ team: parsed.team,
733
+ allow_fresh: parsed.allow_fresh,
734
+ json: parsed.json,
735
+ }
736
+ }
737
+
738
+ fn start_agent_args(args: &[String], cwd: &Path) -> Result<StartAgentArgs, CliError> {
739
+ let parsed = parse_args(args);
740
+ Ok(StartAgentArgs {
741
+ agent: required_pos(&parsed, 0, "agent")?,
742
+ workspace: workspace(&parsed, cwd),
743
+ team: parsed.team,
744
+ force: parsed.force,
745
+ allow_fresh: parsed.allow_fresh,
746
+ no_display: parsed.no_display,
747
+ json: parsed.json,
748
+ })
749
+ }
750
+
751
+ fn stop_agent_args(args: &[String], cwd: &Path) -> Result<StopAgentArgs, CliError> {
752
+ let parsed = parse_args(args);
753
+ Ok(StopAgentArgs {
754
+ agent: required_pos(&parsed, 0, "agent")?,
755
+ workspace: workspace(&parsed, cwd),
756
+ team: parsed.team,
757
+ json: parsed.json,
758
+ })
759
+ }
760
+
761
+ fn reset_agent_args(args: &[String], cwd: &Path) -> Result<ResetAgentArgs, CliError> {
762
+ let parsed = parse_args(args);
763
+ Ok(ResetAgentArgs {
764
+ agent: required_pos(&parsed, 0, "agent")?,
765
+ workspace: workspace(&parsed, cwd),
766
+ team: parsed.team,
767
+ discard_session: parsed.discard_session,
768
+ no_display: parsed.no_display,
769
+ json: parsed.json,
770
+ })
771
+ }
772
+
773
+ fn add_agent_args(args: &[String], cwd: &Path) -> Result<AddAgentArgs, CliError> {
774
+ let parsed = parse_args(args);
775
+ Ok(AddAgentArgs {
776
+ agent: required_pos(&parsed, 0, "agent")?,
777
+ workspace: workspace(&parsed, cwd),
778
+ team: parsed.team,
779
+ role_file: parsed.role_file.ok_or_else(|| CliError::Usage("missing --role-file".to_string()))?,
780
+ no_display: parsed.no_display,
781
+ json: parsed.json,
782
+ })
783
+ }
784
+
785
+ fn fork_agent_args(args: &[String], cwd: &Path) -> Result<ForkAgentArgs, CliError> {
786
+ let parsed = parse_args(args);
787
+ Ok(ForkAgentArgs {
788
+ source_agent: required_pos(&parsed, 0, "source_agent")?,
789
+ workspace: workspace(&parsed, cwd),
790
+ team: parsed.team,
791
+ as_agent: parsed.as_agent.ok_or_else(|| CliError::Usage("missing --as".to_string()))?,
792
+ label: parsed.label,
793
+ no_display: parsed.no_display,
794
+ json: parsed.json,
795
+ })
796
+ }
797
+
798
+ fn remove_agent_args(args: &[String], cwd: &Path) -> Result<RemoveAgentArgs, CliError> {
799
+ let parsed = parse_args(args);
800
+ Ok(RemoveAgentArgs {
801
+ agent: required_pos(&parsed, 0, "agent")?,
802
+ workspace: workspace(&parsed, cwd),
803
+ team: parsed.team,
804
+ from_spec: parsed.from_spec,
805
+ confirm: parsed.confirm,
806
+ force: parsed.force,
807
+ json: parsed.json,
808
+ })
809
+ }
810
+
811
+ fn stuck_list_args(args: &[String], cwd: &Path) -> StuckListArgs {
812
+ let parsed = parse_args(args);
813
+ StuckListArgs {
814
+ workspace: workspace(&parsed, cwd),
815
+ json: parsed.json,
816
+ }
817
+ }
818
+
819
+ fn stuck_cancel_args(args: &[String], cwd: &Path) -> Result<StuckCancelArgs, CliError> {
820
+ let parsed = parse_args(args);
821
+ Ok(StuckCancelArgs {
822
+ agent: required_pos(&parsed, 0, "agent")?,
823
+ workspace: workspace(&parsed, cwd),
824
+ alert_type: alert_type(parsed.alert_type.as_deref())?,
825
+ json: parsed.json,
826
+ })
827
+ }
828
+
829
+ fn alert_type(raw: Option<&str>) -> Result<Option<AlertType>, CliError> {
830
+ match raw {
831
+ Some("stuck") => Ok(Some(AlertType::Stuck)),
832
+ Some("idle_fallback") => Ok(Some(AlertType::IdleFallback)),
833
+ Some("cross_worker_deadlock") => Ok(Some(AlertType::CrossWorkerDeadlock)),
834
+ Some("all") | None => Ok(None),
835
+ Some(other) => Err(CliError::Usage(format!("invalid --alert-type: {other}"))),
836
+ }
837
+ }
838
+
839
+ fn acknowledge_idle_args(args: &[String], cwd: &Path) -> AcknowledgeIdleArgs {
840
+ let parsed = parse_args(args);
841
+ let workspace = workspace(&parsed, cwd);
842
+ AcknowledgeIdleArgs {
843
+ team: parsed.team,
844
+ workspace,
845
+ json: parsed.json,
846
+ }
847
+ }
848
+
849
+ fn doctor_args(args: &[String], cwd: &Path) -> DoctorArgs {
850
+ let parsed = parse_args(args);
851
+ DoctorArgs {
852
+ spec: parsed.positionals.first().map(PathBuf::from),
853
+ workspace: workspace(&parsed, cwd),
854
+ gate: doctor_gate(parsed.gate.as_deref()),
855
+ comms: parsed.comms,
856
+ team: parsed.team,
857
+ fix: parsed.fix,
858
+ fix_schema: parsed.fix_schema,
859
+ cleanup_orphans: parsed.cleanup_orphans,
860
+ confirm: parsed.confirm,
861
+ json: parsed.json,
862
+ }
863
+ }
864
+
865
+ fn doctor_gate(raw: Option<&str>) -> Option<DoctorGate> {
866
+ match raw {
867
+ Some("orphans") => Some(DoctorGate::Orphans),
868
+ Some("comms") => Some(DoctorGate::Comms),
869
+ _ => None,
870
+ }
871
+ }
872
+
873
+ fn sessions_args(args: &[String], cwd: &Path) -> SessionsArgs {
874
+ let parsed = parse_args(args);
875
+ SessionsArgs {
876
+ workspace: workspace(&parsed, cwd),
877
+ json: parsed.json,
878
+ }
879
+ }
880
+
881
+ fn validate_args(args: &[String], cwd: &Path) -> ValidateArgs {
882
+ let parsed = parse_args(args);
883
+ ValidateArgs {
884
+ spec: parsed
885
+ .positionals
886
+ .first()
887
+ .map(PathBuf::from)
888
+ .unwrap_or_else(|| cwd.join("team.spec.yaml")),
889
+ json: parsed.json,
890
+ }
891
+ }
892
+
893
+ fn profile_args(args: &[String], cwd: &Path) -> Result<ProfileArgs, CliError> {
894
+ let parsed = parse_args(args);
895
+ let workspace = resolve_cli_path(cwd, &workspace(&parsed, cwd));
896
+ Ok(ProfileArgs {
897
+ command: required_pos(&parsed, 0, "profile command")?,
898
+ name: required_pos(&parsed, 1, "profile name")?,
899
+ workspace: resolve_path(&workspace),
900
+ team: parsed.team,
901
+ auth_mode: parsed.auth_mode,
902
+ json: parsed.json,
903
+ })
904
+ }
905
+
906
+ fn validate_result_args(args: &[String]) -> Result<ValidateResultArgs, CliError> {
907
+ let parsed = parse_args(args);
908
+ Ok(ValidateResultArgs {
909
+ envelope: parsed.positionals.first().cloned(),
910
+ file: parsed.file,
911
+ result: parsed.result,
912
+ json: parsed.json,
913
+ })
914
+ }
915
+
916
+ fn collect_args(args: &[String], cwd: &Path) -> CollectArgs {
917
+ let parsed = parse_args(args);
918
+ CollectArgs {
919
+ workspace: workspace(&parsed, cwd),
920
+ result_file: parsed.result_file,
921
+ json: parsed.json,
922
+ }
923
+ }
924
+
925
+ fn settle_args(args: &[String], cwd: &Path) -> SettleArgs {
926
+ let parsed = parse_args(args);
927
+ SettleArgs {
928
+ workspace: workspace(&parsed, cwd),
929
+ json: parsed.json,
930
+ }
931
+ }
932
+
933
+ fn repair_state_args(args: &[String], cwd: &Path) -> Result<RepairStateArgs, CliError> {
934
+ let parsed = parse_args(args);
935
+ Ok(RepairStateArgs {
936
+ workspace: workspace(&parsed, cwd),
937
+ task_id: parsed.task.ok_or_else(|| CliError::Usage("missing --task".to_string()))?,
938
+ assignee: parsed.assignee,
939
+ status: parsed
940
+ .status_value
941
+ .ok_or_else(|| CliError::Usage("missing --status".to_string()))?,
942
+ summary: option_value(args, "--summary").or_else(|| parsed.positionals.first().cloned()),
943
+ json: parsed.json,
944
+ })
945
+ }
946
+
947
+ fn option_value(args: &[String], flag: &str) -> Option<String> {
948
+ let prefix = format!("{flag}=");
949
+ let mut i = 0usize;
950
+ while i < args.len() {
951
+ let arg = args.get(i)?;
952
+ if let Some(value) = arg.strip_prefix(&prefix) {
953
+ return Some(value.to_string());
954
+ }
955
+ if arg == flag {
956
+ return args
957
+ .get(i.saturating_add(1))
958
+ .filter(|value| !value.starts_with('-'))
959
+ .cloned();
960
+ }
961
+ i = i.saturating_add(1);
962
+ }
963
+ None
964
+ }
965
+
966
+ fn diagnose_args(args: &[String], cwd: &Path) -> DiagnoseArgs {
967
+ let parsed = parse_args(args);
968
+ DiagnoseArgs {
969
+ workspace: workspace(&parsed, cwd),
970
+ json: parsed.json,
971
+ }
972
+ }
973
+
974
+ fn preflight_args(args: &[String], cwd: &Path) -> PreflightArgs {
975
+ let parsed = parse_args(args);
976
+ let team = parsed
977
+ .team
978
+ .as_deref()
979
+ .map(PathBuf::from)
980
+ .or_else(|| parsed.positionals.first().map(PathBuf::from))
981
+ .unwrap_or_else(|| cwd.to_path_buf());
982
+ PreflightArgs {
983
+ team,
984
+ json: parsed.json,
985
+ }
986
+ }
987
+
988
+ fn wait_ready_args(args: &[String], cwd: &Path) -> WaitReadyArgs {
989
+ let parsed = parse_args(args);
990
+ WaitReadyArgs {
991
+ workspace: workspace(&parsed, cwd),
992
+ timeout: parsed.timeout.unwrap_or(60.0),
993
+ json: parsed.json,
994
+ }
995
+ }
996
+
997
+ fn e2e_args(args: &[String], cwd: &Path) -> E2eArgs {
998
+ let parsed = parse_args(args);
999
+ let providers = parsed
1000
+ .providers
1001
+ .as_deref()
1002
+ .unwrap_or("fake")
1003
+ .split(',')
1004
+ .filter_map(|p| {
1005
+ let trimmed = p.trim();
1006
+ if trimmed.is_empty() {
1007
+ None
1008
+ } else {
1009
+ Some(trimmed.to_string())
1010
+ }
1011
+ })
1012
+ .collect();
1013
+ E2eArgs {
1014
+ workspace: workspace(&parsed, cwd),
1015
+ providers,
1016
+ real: parsed.real,
1017
+ json: parsed.json,
1018
+ }
1019
+ }
1020
+
1021
+ fn peek_args(args: &[String], cwd: &Path) -> Result<PeekArgs, CliError> {
1022
+ let parsed = parse_args(args);
1023
+ Ok(PeekArgs {
1024
+ agent: required_pos(&parsed, 0, "agent")?,
1025
+ workspace: workspace(&parsed, cwd),
1026
+ tail: parsed.tail.unwrap_or(80),
1027
+ allow_raw_screen: parsed.allow_raw_screen,
1028
+ json: parsed.json,
1029
+ })
1030
+ }
1031
+ fn run_coordinator(args: &[String], cwd: &Path) -> Result<ExitCode, CliError> {
1032
+ let parsed = parse_args(args);
1033
+ let workspace = crate::coordinator::WorkspacePath::new(workspace(&parsed, cwd));
1034
+ crate::coordinator::run_daemon(crate::coordinator::DaemonArgs {
1035
+ workspace,
1036
+ once: parsed.once,
1037
+ tick_interval_sec: parsed.tick_interval,
1038
+ })
1039
+ .map(|()| ExitCode::Ok)
1040
+ .map_err(|e| CliError::Runtime(e.to_string()))
1041
+ }
1042
+
1043
+ fn human_value(value: &Value) -> String {
1044
+ match value {
1045
+ Value::Null => "None".to_string(),
1046
+ Value::Bool(true) => "True".to_string(),
1047
+ Value::Bool(false) => "False".to_string(),
1048
+ Value::Number(n) => n.to_string(),
1049
+ Value::String(s) => s.clone(),
1050
+ Value::Array(_) | Value::Object(_) => json_dumps_like(value),
1051
+ }
1052
+ }
1053
+
1054
+ fn json_dumps_like(value: &Value) -> String {
1055
+ match value {
1056
+ Value::Null => "null".to_string(),
1057
+ Value::Bool(b) => b.to_string(),
1058
+ Value::Number(n) => n.to_string(),
1059
+ Value::String(s) => match serde_json::to_string(s) {
1060
+ Ok(text) => text,
1061
+ Err(_) => "\"\"".to_string(),
1062
+ },
1063
+ Value::Array(arr) => {
1064
+ let inner = arr.iter().map(json_dumps_like).collect::<Vec<_>>().join(", ");
1065
+ format!("[{inner}]")
1066
+ }
1067
+ Value::Object(obj) => {
1068
+ let inner = obj
1069
+ .iter()
1070
+ .map(|(k, v)| format!("{}: {}", json_dumps_like(&Value::String(k.clone())), json_dumps_like(v)))
1071
+ .collect::<Vec<_>>()
1072
+ .join(", ");
1073
+ format!("{{{inner}}}")
1074
+ }
1075
+ }
1076
+ }
1077
+
1078
+ #[cfg(test)]
1079
+ mod tests {
1080
+ #![allow(clippy::unwrap_used)]
1081
+
1082
+ use super::*;
1083
+
1084
+ fn tmp_workspace() -> PathBuf {
1085
+ use std::sync::atomic::{AtomicU64, Ordering};
1086
+ static CTR: AtomicU64 = AtomicU64::new(0);
1087
+ let dir = std::env::temp_dir().join(format!(
1088
+ "ta-cli-emit-test-{}-{}",
1089
+ std::process::id(),
1090
+ CTR.fetch_add(1, Ordering::Relaxed)
1091
+ ));
1092
+ std::fs::create_dir_all(&dir).unwrap();
1093
+ dir
1094
+ }
1095
+
1096
+ fn cli_argv(items: &[&str]) -> Vec<String> {
1097
+ items.iter().map(|s| (*s).to_string()).collect()
1098
+ }
1099
+
1100
+ fn source_dispatch_commands() -> Vec<&'static str> {
1101
+ let source = include_str!("emit.rs");
1102
+ let after_start = source.split_once("fn dispatch(").unwrap().1;
1103
+ let dispatch_source = after_start.split_once("const DISPATCH_COMMANDS").unwrap().0;
1104
+ let mut commands = Vec::new();
1105
+ for line in dispatch_source.lines() {
1106
+ let line = line.trim_start();
1107
+ let Some(rest) = line.strip_prefix('"') else {
1108
+ continue;
1109
+ };
1110
+ let Some((command, after_command)) = rest.split_once('"') else {
1111
+ continue;
1112
+ };
1113
+ let after_command = after_command.trim_start();
1114
+ if (after_command.starts_with("=>") || after_command.starts_with("if "))
1115
+ && !commands.contains(&command)
1116
+ {
1117
+ commands.push(command);
1118
+ }
1119
+ }
1120
+ commands
1121
+ }
1122
+
1123
+ #[test]
1124
+ fn t0_help_catalog_tracks_dispatch_commands() {
1125
+ let source_commands = source_dispatch_commands();
1126
+ for command in &source_commands {
1127
+ assert!(
1128
+ DISPATCH_COMMANDS.contains(command),
1129
+ "dispatch command `{command}` is missing from DISPATCH_COMMANDS"
1130
+ );
1131
+ }
1132
+ for command in DISPATCH_COMMANDS {
1133
+ assert!(
1134
+ source_commands.contains(command),
1135
+ "DISPATCH_COMMANDS contains `{command}` but dispatch has no matching arm"
1136
+ );
1137
+ }
1138
+
1139
+ let top_help = command_help(None);
1140
+ for command in DISPATCH_COMMANDS {
1141
+ assert!(
1142
+ top_help.contains(command),
1143
+ "top-level --help is missing dispatch command `{command}`"
1144
+ );
1145
+ let command_help = command_help(Some(command));
1146
+ assert!(
1147
+ command_help.contains("usage: team-agent") && command_help.contains(command),
1148
+ "`team-agent {command} --help` must show command-specific usage, got {command_help:?}"
1149
+ );
1150
+ }
1151
+ for command in SPEC_ONLY_HELP_COMMANDS {
1152
+ assert!(
1153
+ top_help.contains(command),
1154
+ "top-level --help is missing spec-only help command `{command}`"
1155
+ );
1156
+ }
1157
+ }
1158
+
1159
+ #[test]
1160
+ fn t0_help_catalog_lists_command_flags() {
1161
+ for (command, flags) in [
1162
+ ("quick-start", &["--workspace", "--team-id", "--yes", "--fresh", "--json"][..]),
1163
+ ("send", &["--workspace", "--team", "--targets", "--watch-result", "--timeout", "--json"][..]),
1164
+ ("status", &["--workspace", "--summary", "--json", "--detail"][..]),
1165
+ ("shutdown", &["--workspace", "--team", "--keep-logs", "--json"][..]),
1166
+ ("restart", &["--team", "--allow-fresh", "--json"][..]),
1167
+ ("start-agent", &["--workspace", "--team", "--force", "--allow-fresh", "--no-display", "--json"][..]),
1168
+ ("reset-agent", &["--workspace", "--team", "--discard-session", "--no-display", "--json"][..]),
1169
+ ("add-agent", &["--role-file", "--workspace", "--team", "--no-display", "--json"][..]),
1170
+ ("fork-agent", &["--as", "--label", "--workspace", "--team", "--no-display", "--json"][..]),
1171
+ ("remove-agent", &["--workspace", "--team", "--from-spec", "--confirm", "--force", "--json"][..]),
1172
+ ("doctor", &["--workspace", "--team", "--gate", "--fix-schema", "--cleanup-orphans", "--json"][..]),
1173
+ ("collect", &["--workspace", "--result-file", "--json"][..]),
1174
+ ("repair-state", &["--task", "--status", "--assignee", "--workspace", "--json"][..]),
1175
+ ("wait-ready", &["--workspace", "--timeout", "--json"][..]),
1176
+ ("peek", &["--workspace", "--tail", "--allow-raw-screen", "--json"][..]),
1177
+ ("coordinator", &["--workspace", "--once", "--tick-interval"][..]),
1178
+ ] {
1179
+ let help = command_help(Some(command));
1180
+ for flag in flags {
1181
+ assert!(help.contains(flag), "`team-agent {command} --help` is missing {flag}");
1182
+ }
1183
+ }
1184
+ }
1185
+
1186
+ #[test]
1187
+ fn ux_quick_start_workspace_resolves_relative_agents_dir_inside_workspace() {
1188
+ let cwd = tmp_workspace();
1189
+ let ws = tmp_workspace();
1190
+ let args = quick_start_args(
1191
+ &cli_argv(&["--workspace", &ws.to_string_lossy(), "agents", "--yes", "--json"]),
1192
+ &cwd,
1193
+ )
1194
+ .unwrap();
1195
+ assert_eq!(
1196
+ args.agents_dir,
1197
+ ws.join("agents"),
1198
+ "quick-start --workspace <ws> agents must resolve the role-doc dir under <ws>, so team-in-team \
1199
+ setup works from any caller cwd"
1200
+ );
1201
+ let _ = std::fs::remove_dir_all(&cwd);
1202
+ let _ = std::fs::remove_dir_all(&ws);
1203
+ }
1204
+ }