@team-agent/installer 0.2.10 → 0.3.0

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 +1077 -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 +1141 -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 +436 -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 +1063 -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 +525 -0
  59. package/crates/team-agent/src/leader/rediscover.rs +1099 -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 +234 -0
  64. package/crates/team-agent/src/leader/tests/identity.rs +206 -0
  65. package/crates/team-agent/src/leader/tests/idle.rs +271 -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 +253 -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 +487 -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 +1833 -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 +933 -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 +685 -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 +159 -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 +388 -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 +542 -0
  110. package/crates/team-agent/src/messaging/helpers.rs +209 -0
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +340 -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 +537 -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 +582 -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 +656 -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 +586 -0
  172. package/crates/team-agent/src/tmux_backend.rs +758 -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 +90 -106
  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 -83
  203. package/src/team_agent/coordinator/lifecycle.py +0 -363
  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 -200
  226. package/src/team_agent/idle_takeover.py +0 -59
  227. package/src/team_agent/idle_takeover_wiring.py +0 -111
  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 -254
  255. package/src/team_agent/messaging/delivery.py +0 -473
  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 -457
  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 -86
  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 -1239
  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 -143
  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 -602
  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,306 @@
1
+ //! `profile {init,doctor,show}` CLI verb.
2
+
3
+ use super::*;
4
+
5
+ const SECRET_BOUNDARY: &str = "# Team Agent Profile Secret Boundary\n";
6
+
7
+ pub fn cmd_profile(args: &ProfileArgs) -> Result<CmdResult, CliError> {
8
+ let value = match args.command.as_str() {
9
+ "init" => init_profile(args)?,
10
+ "doctor" => doctor_profile(args)?,
11
+ "show" => show_profile(args)?,
12
+ other => {
13
+ return Err(CliError::Usage(format!("invalid profile command: {other}")));
14
+ }
15
+ };
16
+ Ok(CmdResult::from_json(value, args.json))
17
+ }
18
+
19
+ fn init_profile(args: &ProfileArgs) -> Result<Value, CliError> {
20
+ let auth_mode = args.auth_mode.as_deref().unwrap_or("subscription");
21
+ validate_auth_mode(auth_mode)?;
22
+ let dir = profile_dir(args);
23
+ std::fs::create_dir_all(&dir)?;
24
+ let path = dir.join(format!("{}.env", args.name));
25
+ let template_path = dir.join(format!("{}.example.env", args.name));
26
+ let body = profile_template(&args.name, auth_mode);
27
+ let created_profile = write_new_file(&path, &body)?;
28
+ if created_profile {
29
+ set_owner_only_permissions(&path)?;
30
+ }
31
+ let created_template = write_new_file(&template_path, &body)?;
32
+ write_boundary_files(&dir)?;
33
+
34
+ let mut obj = Map::new();
35
+ obj.insert("ok".to_string(), Value::Bool(true));
36
+ obj.insert("profile".to_string(), Value::String(args.name.clone()));
37
+ obj.insert("auth_mode".to_string(), Value::String(auth_mode.to_string()));
38
+ obj.insert("path".to_string(), path_value(&path));
39
+ obj.insert("template_path".to_string(), path_value(&template_path));
40
+ obj.insert("created_profile".to_string(), Value::Bool(created_profile));
41
+ obj.insert("created_template".to_string(), Value::Bool(created_template));
42
+ obj.insert("secret_written".to_string(), Value::Bool(created_profile));
43
+ obj.insert(
44
+ "safe_inspection_command".to_string(),
45
+ Value::String(safe_inspection_command(args)),
46
+ );
47
+ obj.insert(
48
+ "raw_file_read_allowed_for_agents".to_string(),
49
+ Value::Bool(false),
50
+ );
51
+ obj.insert(
52
+ "instruction".to_string(),
53
+ Value::String("Inspect credentials with team-agent profile show; do not read raw profile files from agent context.".to_string()),
54
+ );
55
+ Ok(Value::Object(obj))
56
+ }
57
+
58
+ fn doctor_profile(args: &ProfileArgs) -> Result<Value, CliError> {
59
+ let dir = profile_dir(args);
60
+ let path = dir.join(format!("{}.env", args.name));
61
+ let template_path = dir.join(format!("{}.example.env", args.name));
62
+ if !path.exists() {
63
+ return Ok(missing_profile_value(args, &path, Some(&template_path)));
64
+ }
65
+ let values = read_profile_env(&path)?;
66
+ let auth_mode =
67
+ profile_value(&values, "AUTH_MODE").unwrap_or_else(|| "subscription".to_string());
68
+ let secret_keys = secret_keys_present(&values);
69
+ let keys = sorted_keys(&values);
70
+ let mut obj = Map::new();
71
+ obj.insert("ok".to_string(), Value::Bool(true));
72
+ obj.insert("path".to_string(), path_value(&path));
73
+ obj.insert("profile".to_string(), Value::String(args.name.clone()));
74
+ obj.insert(
75
+ "credential_present".to_string(),
76
+ Value::Bool(credential_present(&values)),
77
+ );
78
+ obj.insert("auth_mode".to_string(), Value::String(auth_mode));
79
+ obj.insert("keys_present".to_string(), string_array(keys));
80
+ obj.insert(
81
+ "raw_file_read_allowed_for_agents".to_string(),
82
+ Value::Bool(false),
83
+ );
84
+ obj.insert(
85
+ "redaction_engine".to_string(),
86
+ Value::String("key_name".to_string()),
87
+ );
88
+ obj.insert("safe_for_agent_context".to_string(), Value::Bool(true));
89
+ obj.insert(
90
+ "safe_inspection_command".to_string(),
91
+ Value::String(safe_inspection_command(args)),
92
+ );
93
+ obj.insert("secret_keys_present".to_string(), string_array(secret_keys));
94
+ obj.insert("secret_values_printed".to_string(), Value::Bool(false));
95
+ obj.insert("suggestion".to_string(), Value::Null);
96
+ obj.insert("template_path".to_string(), path_value(&template_path));
97
+ Ok(Value::Object(obj))
98
+ }
99
+
100
+ fn show_profile(args: &ProfileArgs) -> Result<Value, CliError> {
101
+ let path = profile_dir(args).join(format!("{}.env", args.name));
102
+ if !path.exists() {
103
+ return Ok(missing_profile_value(args, &path, None));
104
+ }
105
+ let values = read_profile_env(&path)?;
106
+ let auth_mode =
107
+ profile_value(&values, "AUTH_MODE").unwrap_or_else(|| "subscription".to_string());
108
+ let secret_keys = secret_keys_present(&values);
109
+ let missing_common = missing_common_keys(&values, &auth_mode);
110
+ let mut obj = Map::new();
111
+ obj.insert("ok".to_string(), Value::Bool(true));
112
+ obj.insert("profile".to_string(), Value::String(args.name.clone()));
113
+ obj.insert(
114
+ "credential_present".to_string(),
115
+ Value::Bool(credential_present(&values)),
116
+ );
117
+ obj.insert("auth_mode".to_string(), Value::String(auth_mode));
118
+ obj.insert("values".to_string(), redacted_values(&values));
119
+ obj.insert("keys_present".to_string(), string_array(sorted_keys(&values)));
120
+ obj.insert("secret_keys_present".to_string(), string_array(secret_keys));
121
+ obj.insert("missing_common".to_string(), string_array(missing_common));
122
+ obj.insert("safe_for_agent_context".to_string(), Value::Bool(true));
123
+ obj.insert("secret_values_printed".to_string(), Value::Bool(false));
124
+ obj.insert(
125
+ "raw_file_read_allowed_for_agents".to_string(),
126
+ Value::Bool(false),
127
+ );
128
+ obj.insert(
129
+ "instruction".to_string(),
130
+ Value::String("Values are redacted for safe inspection by agents.".to_string()),
131
+ );
132
+ Ok(Value::Object(obj))
133
+ }
134
+
135
+ fn profile_dir(args: &ProfileArgs) -> PathBuf {
136
+ let _ = &args.team;
137
+ args.workspace.join(".team").join("current").join("profiles")
138
+ }
139
+
140
+ fn validate_auth_mode(auth_mode: &str) -> Result<(), CliError> {
141
+ match auth_mode {
142
+ "compatible_api" | "official_api" | "subscription" => Ok(()),
143
+ other => Err(CliError::Usage(format!("invalid --auth-mode: {other}"))),
144
+ }
145
+ }
146
+
147
+ fn profile_template(name: &str, auth_mode: &str) -> String {
148
+ match auth_mode {
149
+ "compatible_api" | "official_api" => {
150
+ format!("AUTH_MODE={auth_mode}\nPROFILE_NAME={name}\nAPI_KEY=\nMODEL=\n")
151
+ }
152
+ _ => format!("AUTH_MODE=subscription\nPROFILE_NAME={name}\n"),
153
+ }
154
+ }
155
+
156
+ fn write_new_file(path: &Path, body: &str) -> Result<bool, CliError> {
157
+ if path.exists() {
158
+ return Ok(false);
159
+ }
160
+ std::fs::write(path, body)?;
161
+ Ok(true)
162
+ }
163
+
164
+ fn write_boundary_files(dir: &Path) -> Result<(), CliError> {
165
+ for name in ["AGENTS.md", "CLAUDE.md"] {
166
+ write_new_file(&dir.join(name), SECRET_BOUNDARY)?;
167
+ }
168
+ Ok(())
169
+ }
170
+
171
+ #[cfg(unix)]
172
+ fn set_owner_only_permissions(path: &Path) -> Result<(), CliError> {
173
+ use std::os::unix::fs::PermissionsExt as _;
174
+ let mut permissions = std::fs::metadata(path)?.permissions();
175
+ permissions.set_mode(0o600);
176
+ std::fs::set_permissions(path, permissions)?;
177
+ Ok(())
178
+ }
179
+
180
+ #[cfg(not(unix))]
181
+ fn set_owner_only_permissions(path: &Path) -> Result<(), CliError> {
182
+ let _ = path;
183
+ Ok(())
184
+ }
185
+
186
+ fn read_profile_env(path: &Path) -> Result<Map<String, Value>, CliError> {
187
+ let raw = std::fs::read_to_string(path)?;
188
+ let mut values = Map::new();
189
+ for line in raw.lines() {
190
+ let trimmed = line.trim();
191
+ if trimmed.is_empty() || trimmed.starts_with('#') {
192
+ continue;
193
+ }
194
+ let Some((key, value)) = trimmed.split_once('=') else {
195
+ continue;
196
+ };
197
+ values.insert(key.to_string(), Value::String(value.to_string()));
198
+ }
199
+ Ok(values)
200
+ }
201
+
202
+ fn redacted_values(values: &Map<String, Value>) -> Value {
203
+ let mut out = Map::new();
204
+ for key in sorted_keys(values) {
205
+ let value = values.get(&key).and_then(Value::as_str).unwrap_or("");
206
+ let mut item = Map::new();
207
+ item.insert("present".to_string(), Value::Bool(!value.is_empty()));
208
+ if is_secret_key(&key) {
209
+ item.insert("redacted".to_string(), Value::Bool(true));
210
+ } else {
211
+ item.insert("value".to_string(), Value::String(value.to_string()));
212
+ }
213
+ out.insert(key, Value::Object(item));
214
+ }
215
+ Value::Object(out)
216
+ }
217
+
218
+ fn profile_value(values: &Map<String, Value>, key: &str) -> Option<String> {
219
+ values
220
+ .get(key)
221
+ .and_then(Value::as_str)
222
+ .map(ToString::to_string)
223
+ }
224
+
225
+ fn missing_profile_value(args: &ProfileArgs, path: &Path, template_path: Option<&Path>) -> Value {
226
+ let mut obj = Map::new();
227
+ obj.insert("ok".to_string(), Value::Bool(false));
228
+ obj.insert("profile".to_string(), Value::String(args.name.clone()));
229
+ obj.insert("path".to_string(), path_value(path));
230
+ if let Some(template) = template_path {
231
+ obj.insert("template_path".to_string(), path_value(template));
232
+ }
233
+ obj.insert(
234
+ "suggestion".to_string(),
235
+ Value::String(format!(
236
+ "Run team-agent profile init {} --auth-mode subscription.",
237
+ args.name
238
+ )),
239
+ );
240
+ obj.insert(
241
+ "safe_inspection_command".to_string(),
242
+ Value::String(safe_inspection_command(args)),
243
+ );
244
+ Value::Object(obj)
245
+ }
246
+
247
+ fn safe_inspection_command(args: &ProfileArgs) -> String {
248
+ format!(
249
+ "team-agent profile show {} --workspace {}",
250
+ args.name,
251
+ args.workspace.display()
252
+ )
253
+ }
254
+
255
+ fn path_value(path: &Path) -> Value {
256
+ Value::String(path.to_string_lossy().to_string())
257
+ }
258
+
259
+ fn sorted_keys(values: &Map<String, Value>) -> Vec<String> {
260
+ let mut keys = values.keys().cloned().collect::<Vec<_>>();
261
+ keys.sort();
262
+ keys
263
+ }
264
+
265
+ fn string_array(values: Vec<String>) -> Value {
266
+ Value::Array(values.into_iter().map(Value::String).collect())
267
+ }
268
+
269
+ fn secret_keys_present(values: &Map<String, Value>) -> Vec<String> {
270
+ sorted_keys(values)
271
+ .into_iter()
272
+ .filter(|key| is_secret_key(key))
273
+ .collect()
274
+ }
275
+
276
+ fn credential_present(values: &Map<String, Value>) -> bool {
277
+ values.iter().any(|(key, value)| {
278
+ is_secret_key(key) && value.as_str().is_some_and(|raw| !raw.is_empty())
279
+ })
280
+ }
281
+
282
+ fn missing_common_keys(values: &Map<String, Value>, auth_mode: &str) -> Vec<String> {
283
+ let required = match auth_mode {
284
+ "compatible_api" | "official_api" => ["API_KEY", "MODEL"].as_slice(),
285
+ _ => ["AUTH_MODE", "PROFILE_NAME"].as_slice(),
286
+ };
287
+ required
288
+ .iter()
289
+ .filter(|key| {
290
+ values
291
+ .get(**key)
292
+ .and_then(Value::as_str)
293
+ .is_none_or(str::is_empty)
294
+ })
295
+ .map(|key| (*key).to_string())
296
+ .collect()
297
+ }
298
+
299
+ fn is_secret_key(key: &str) -> bool {
300
+ let upper = key.to_ascii_uppercase();
301
+ upper.contains("SECRET")
302
+ || upper.contains("TOKEN")
303
+ || upper.contains("PASSWORD")
304
+ || upper.contains("API_KEY")
305
+ || upper.ends_with("_KEY")
306
+ }
@@ -0,0 +1,215 @@
1
+ //! cli · send — `cmd_send` + target 解析(`_send_target`)+ `SendArgs`→`SendOptions` 翻译
2
+ //! (`send_options_from_args`,旗标取反语义)。
3
+
4
+ use super::*;
5
+ use crate::messaging::{DeliveryOutcome, DeliveryRefusal, DeliveryStage, DeliveryStatus};
6
+
7
+ /// `cmd_send`(`commands.py:164`)。解析 target(`--to` fanout / 单 target / `*`)→ [`MessageTarget`],
8
+ /// 拼 [`SendOptions`](no_ack→requires_ack 取反、no_wait→wait_visible 取反等)→ `messaging::send_message`。
9
+ pub fn cmd_send(args: &SendArgs) -> Result<CmdResult, CliError> {
10
+ let selected = crate::state::selector::resolve_active_team(
11
+ &args.workspace,
12
+ args.team.as_deref(),
13
+ crate::state::selector::SelectorMode::RuntimeOnly,
14
+ )?;
15
+ let target = send_target(args.targets.as_deref(), args.target.as_deref());
16
+ let mut opts = send_options_from_args(args);
17
+ if opts.team.is_none() {
18
+ opts.team = Some(TeamKey::new(selected.team_key.clone()));
19
+ }
20
+ let content = args.message.join(" ");
21
+ // CR-061/N27 routing-ambiguous: a single positional with no `--to`/`--targets` and an
22
+ // empty message body is a prompt-only invocation (`team-agent send "fix the build"`).
23
+ // The lone positional is CONTENT, not a target — reject with `routing_ambiguous`
24
+ // (NOT `target_not_in_team`, which would lie that the user did pick a target).
25
+ if let Some(amb) = routing_ambiguous_value(&selected.run_workspace, args, &target, &content, &opts) {
26
+ return Ok(CmdResult::from_json(amb, args.json));
27
+ }
28
+ let outcome = messaging::send_message(&selected.run_workspace, &target, &content, &opts)?;
29
+ let mut value = delivery_outcome_json(&outcome, &target, &content, &opts);
30
+ if opts.watch_result {
31
+ if let Some(obj) = value.as_object_mut() {
32
+ obj.insert("watch".to_string(), watch_notice_json(&target, &opts));
33
+ }
34
+ }
35
+ Ok(CmdResult::from_json(value, args.json))
36
+ }
37
+
38
+ fn routing_ambiguous_value(
39
+ workspace: &Path,
40
+ args: &SendArgs,
41
+ target: &MessageTarget,
42
+ content: &str,
43
+ opts: &SendOptions,
44
+ ) -> Option<Value> {
45
+ if args.targets.is_some() || !content.is_empty() {
46
+ return None;
47
+ }
48
+ let MessageTarget::Single(name) = target else {
49
+ return None;
50
+ };
51
+ if name.is_empty() {
52
+ return None;
53
+ }
54
+ let state = crate::state::persist::load_runtime_state(workspace).ok()?;
55
+ let in_team = state
56
+ .get("agents")
57
+ .and_then(|v| v.as_object())
58
+ .is_some_and(|a| a.contains_key(name));
59
+ if in_team {
60
+ return None;
61
+ }
62
+ Some(json!({
63
+ "ok": false,
64
+ "status": "refused",
65
+ "target": null,
66
+ "agent_id": null,
67
+ "content": name,
68
+ "sender": opts.sender,
69
+ "message_id": null,
70
+ "message_status": "refused",
71
+ "verification": null,
72
+ "stage": null,
73
+ "reason": "routing_ambiguous",
74
+ "channel": null,
75
+ }))
76
+ }
77
+
78
+ /// `_send_target`(`commands.py:181-184`):`--to` comma-split fanout / `target` 单值 / None。
79
+ pub fn send_target(targets: Option<&str>, target: Option<&str>) -> MessageTarget {
80
+ if let Some(targets) = targets.filter(|s| !s.is_empty()) {
81
+ let recipients: Vec<String> = targets
82
+ .split(',')
83
+ .map(str::trim)
84
+ .filter(|s| !s.is_empty())
85
+ .map(ToString::to_string)
86
+ .collect();
87
+ return MessageTarget::Fanout(recipients);
88
+ }
89
+ match target {
90
+ Some("*") => MessageTarget::Broadcast,
91
+ Some(target) => MessageTarget::Single(target.to_string()),
92
+ None => MessageTarget::Single(String::new()),
93
+ }
94
+ }
95
+
96
+ /// `cmd_send` 的 [`SendArgs`]→[`SendOptions`] 翻译(`commands.py:170-177`)。CLI **独占**的
97
+ /// 旗标取反语义(经典 off-by-inversion bug 面):`no_ack→!requires_ack`、`no_wait→!wait_visible`、
98
+ /// `watch_result` 直传、`task_id`/`sender`/`confirm_human`/`timeout`/`team` 透传。
99
+ /// (其余 `lock_timeout`/`block_until_delivered` 用 [`SendOptions::default`]。)
100
+ pub fn send_options_from_args(args: &SendArgs) -> SendOptions {
101
+ SendOptions {
102
+ task_id: args.task.as_ref().map(|s| TaskId::new(s.clone())),
103
+ route_task_id: true,
104
+ sender: args.sender.clone(),
105
+ requires_ack: !args.no_ack,
106
+ confirm_human: args.confirm_human,
107
+ wait_visible: !args.no_wait,
108
+ timeout: args.timeout,
109
+ watch_result: args.watch_result,
110
+ team: args.team.as_ref().map(|s| TeamKey::new(s.clone())),
111
+ message_id: args.message_id.clone(),
112
+ ..SendOptions::default()
113
+ }
114
+ }
115
+
116
+ fn watch_notice_json(target: &MessageTarget, opts: &SendOptions) -> Value {
117
+ let agent_id = match target {
118
+ MessageTarget::Single(agent) => agent.clone(),
119
+ MessageTarget::Broadcast => "*".to_string(),
120
+ MessageTarget::Fanout(recipients) => recipients.first().cloned().unwrap_or_else(|| "-".to_string()),
121
+ };
122
+ json!({
123
+ "status": "registered",
124
+ "watcher_id": format!("watch-{agent_id}"),
125
+ "task_id": opts.task_id.as_ref().map(|t| t.as_str().to_string()),
126
+ "agent_id": agent_id,
127
+ "notice": "Team Agent will collect the result and notify the leader when this task reports completion."
128
+ })
129
+ }
130
+
131
+ fn delivery_outcome_json(
132
+ outcome: &DeliveryOutcome,
133
+ target: &MessageTarget,
134
+ content: &str,
135
+ opts: &SendOptions,
136
+ ) -> Value {
137
+ let target_wire = target_json(target);
138
+ json!({
139
+ "ok": outcome.ok,
140
+ "status": delivery_status_wire(outcome.status),
141
+ "target": target_wire,
142
+ "agent_id": first_target(target),
143
+ "content": content,
144
+ "sender": opts.sender,
145
+ "message_id": outcome.message_id,
146
+ "message_status": outcome.message_status.0,
147
+ "verification": outcome.verification,
148
+ "stage": outcome.stage.map(delivery_stage_wire),
149
+ "reason": outcome.reason.map(delivery_refusal_wire),
150
+ "channel": outcome.channel,
151
+ })
152
+ }
153
+
154
+ fn target_json(target: &MessageTarget) -> Value {
155
+ match target {
156
+ MessageTarget::Single(agent) => json!(agent),
157
+ MessageTarget::Broadcast => json!("*"),
158
+ MessageTarget::Fanout(recipients) => json!(recipients),
159
+ }
160
+ }
161
+
162
+ fn first_target(target: &MessageTarget) -> String {
163
+ match target {
164
+ MessageTarget::Single(agent) => agent.clone(),
165
+ MessageTarget::Broadcast => "*".to_string(),
166
+ MessageTarget::Fanout(recipients) => recipients.first().cloned().unwrap_or_default(),
167
+ }
168
+ }
169
+
170
+ fn delivery_status_wire(status: DeliveryStatus) -> &'static str {
171
+ match status {
172
+ DeliveryStatus::Delivered => "delivered",
173
+ DeliveryStatus::Failed => "failed",
174
+ DeliveryStatus::Queued => "queued",
175
+ DeliveryStatus::Blocked => "blocked",
176
+ DeliveryStatus::Refused => "refused",
177
+ DeliveryStatus::RetryScheduled => "retry_scheduled",
178
+ DeliveryStatus::TrustAutoAnswerExhausted => "trust_auto_answer_exhausted",
179
+ DeliveryStatus::AlreadyDelivered => "already_delivered",
180
+ DeliveryStatus::FallbackLog => "fallback_log",
181
+ DeliveryStatus::BroadcastDelivered => "broadcast_delivered",
182
+ DeliveryStatus::BroadcastPartial => "broadcast_partial",
183
+ DeliveryStatus::FanoutDelivered => "fanout_delivered",
184
+ DeliveryStatus::FanoutPartial => "fanout_partial",
185
+ }
186
+ }
187
+
188
+ fn delivery_refusal_wire(reason: DeliveryRefusal) -> &'static str {
189
+ match reason {
190
+ DeliveryRefusal::TargetNotInTeam => "target_not_in_team",
191
+ DeliveryRefusal::HumanConfirmationRequired => "human_confirmation_required",
192
+ DeliveryRefusal::MissingPermissions => "missing_permissions",
193
+ DeliveryRefusal::RecipientBusy => "recipient_busy",
194
+ DeliveryRefusal::UnknownRecipient => "unknown_recipient",
195
+ DeliveryRefusal::TmuxTargetMissing => "tmux_target_missing",
196
+ DeliveryRefusal::MessageAlreadyClaimed => "message_already_claimed",
197
+ DeliveryRefusal::LeaderNotAttached => "leader_not_attached",
198
+ DeliveryRefusal::NoCallerPane => "no_caller_pane",
199
+ DeliveryRefusal::TeamOwnerMismatch => "team_owner_mismatch",
200
+ DeliveryRefusal::Ambiguous => "ambiguous",
201
+ DeliveryRefusal::RecipientPaneInNonInputMode => "recipient_pane_in_non_input_mode",
202
+ DeliveryRefusal::SessionDrift => "session_drift",
203
+ DeliveryRefusal::Duplicate => "duplicate",
204
+ DeliveryRefusal::RoutingAmbiguous => "routing_ambiguous",
205
+ }
206
+ }
207
+
208
+ fn delivery_stage_wire(stage: DeliveryStage) -> &'static str {
209
+ match stage {
210
+ DeliveryStage::TrustAutoAnswerDismissalWait => "trust_auto_answer_dismissal_wait",
211
+ DeliveryStage::Inject => "inject",
212
+ DeliveryStage::Submit => "submit",
213
+ DeliveryStage::VisibleCheck => "visible_check",
214
+ }
215
+ }
@@ -0,0 +1,179 @@
1
+ //! cli · status — 五行 triage summary 渲染 + agent 分类计数(commands.py
2
+ //! `_format_status_summary` / `_agent_summary_counts` / `_interaction_counts` /
3
+ //! `_latest_result_line` + `cmd_status` 的 compact 不变量)。
4
+
5
+ use super::*;
6
+
7
+ /// 单 agent 双信号(`status` × `agent_health.status`)→ 桶分类(`_agent_summary_counts` 内层)。
8
+ /// **穷尽 match,禁默认 idle 臂**(§11)。`raw`/`hstatus` 已 lowercase。
9
+ pub fn classify_agent_bucket(raw_status: &str, health_status: &str) -> SummaryBucket {
10
+ let raw = raw_status.to_ascii_lowercase();
11
+ let health = health_status.to_ascii_lowercase();
12
+ if matches!(raw.as_str(), "failed" | "error") || matches!(health.as_str(), "failed" | "error")
13
+ {
14
+ SummaryBucket::Failed
15
+ } else if matches!(raw.as_str(), "stopped" | "done") || health == "done" {
16
+ SummaryBucket::Stopped
17
+ } else if raw == "busy" || matches!(health.as_str(), "running" | "working") {
18
+ SummaryBucket::Busy
19
+ } else if health == "idle" {
20
+ SummaryBucket::Idle
21
+ } else if raw == "running" {
22
+ SummaryBucket::Running
23
+ } else {
24
+ SummaryBucket::Unknown
25
+ }
26
+ }
27
+
28
+ /// `_agent_summary_counts`(`commands.py:309-330`):遍历 agents×health → [`SummaryCounts`]。
29
+ pub fn agent_summary_counts(agents: &Value, health: &Value) -> SummaryCounts {
30
+ let mut counts = SummaryCounts::default();
31
+ let Some(agent_map) = agents.as_object() else {
32
+ return counts;
33
+ };
34
+ for (agent_id, agent) in agent_map {
35
+ let raw = agent
36
+ .get("status")
37
+ .and_then(Value::as_str)
38
+ .unwrap_or("");
39
+ let hstatus = health
40
+ .get(agent_id)
41
+ .and_then(|v| v.get("status"))
42
+ .and_then(Value::as_str)
43
+ .unwrap_or("");
44
+ match classify_agent_bucket(raw, hstatus) {
45
+ SummaryBucket::Running => counts.running += 1,
46
+ SummaryBucket::Busy => counts.busy += 1,
47
+ SummaryBucket::Idle => counts.idle += 1,
48
+ SummaryBucket::Stopped => counts.stopped += 1,
49
+ SummaryBucket::Failed => counts.failed += 1,
50
+ SummaryBucket::Unknown => counts.unknown += 1,
51
+ }
52
+ }
53
+ counts
54
+ }
55
+
56
+ /// `_interaction_counts`(`commands.py:292-306`):遍历 agents 的 `interacted` 字段。
57
+ pub fn interaction_counts(agents: &Value) -> InteractionCounts {
58
+ let mut counts = InteractionCounts::default();
59
+ let Some(agent_map) = agents.as_object() else {
60
+ return counts;
61
+ };
62
+ for agent in agent_map.values() {
63
+ let interacted = agent
64
+ .get("interacted")
65
+ .and_then(Value::as_str)
66
+ .unwrap_or("");
67
+ if !interacted.is_empty() && interacted != "never" {
68
+ counts.interacted += 1;
69
+ } else {
70
+ counts.never += 1;
71
+ }
72
+ }
73
+ counts
74
+ }
75
+
76
+ /// `_format_status_summary`(`commands.py:263-289`):把 status `Value` 渲染成五行 triage 文本。
77
+ /// **Gap 18a 字节锁(§11)**:line[2] 精确串
78
+ /// `agents: N — running=.. busy=.. idle=.. stopped=.. failed=.. unknown=..`,
79
+ /// `(N interacted, M never)` 仅当 interacted>0 才追加(`commands.py:280-282`)。
80
+ /// 五行:coordinator / receiver / agents / queued / latest result。空格/破折号/顺序禁改。
81
+ pub fn format_status_summary(data: &Value) -> String {
82
+ let coordinator = non_empty_str(
83
+ data.get("coordinator")
84
+ .and_then(|v| v.get("status"))
85
+ .and_then(Value::as_str),
86
+ "stopped",
87
+ );
88
+ let schema_ok = python_truthy(
89
+ data
90
+ .get("coordinator")
91
+ .and_then(|v| v.get("schema_ok"))
92
+ .unwrap_or(&Value::Null),
93
+ );
94
+ let tmux = python_truthy(data.get("tmux_session_present").unwrap_or(&Value::Null));
95
+ let receiver = data.get("leader_receiver").unwrap_or(&Value::Null);
96
+ let pane = non_empty_str(
97
+ receiver.get("pane_id").and_then(Value::as_str),
98
+ "-",
99
+ );
100
+ let cmd = first_non_empty_str(
101
+ &[
102
+ receiver.get("pane_current_command").and_then(Value::as_str),
103
+ receiver.get("current_command").and_then(Value::as_str),
104
+ ],
105
+ "-",
106
+ );
107
+ let agents = data.get("agents").unwrap_or(&Value::Null);
108
+ let health = data.get("agent_health").unwrap_or(&Value::Null);
109
+ let counts = agent_summary_counts(agents, health);
110
+ let interactions = interaction_counts(agents);
111
+ let mut agent_line = format!(
112
+ "agents: {} — running={} busy={} idle={} stopped={} failed={} unknown={}",
113
+ counts.total(),
114
+ counts.running,
115
+ counts.busy,
116
+ counts.idle,
117
+ counts.stopped,
118
+ counts.failed,
119
+ counts.unknown
120
+ );
121
+ if interactions.interacted > 0 {
122
+ agent_line.push_str(&format!(
123
+ " ({} interacted, {} never)",
124
+ interactions.interacted, interactions.never
125
+ ));
126
+ }
127
+ let queued = data
128
+ .get("queued_messages")
129
+ .and_then(Value::as_array)
130
+ .map(Vec::len)
131
+ .unwrap_or(0);
132
+ let latest = data
133
+ .get("latest_results")
134
+ .and_then(Value::as_array)
135
+ .and_then(|arr| arr.first())
136
+ .filter(|v| python_truthy(v))
137
+ .map(format_latest_result)
138
+ .unwrap_or_else(|| "none".to_string());
139
+ format!(
140
+ "coordinator: {coordinator} schema_ok={schema_ok} tmux={tmux}\nreceiver: {pane} cmd={cmd}\n{agent_line}\nqueued: {queued} mailbox messages awaiting delivery\nlatest result: {latest}"
141
+ )
142
+ }
143
+
144
+ /// `_latest_result_line`(`commands.py:333-337`):agent_id/summary/created_at 渲染单行。
145
+ /// summary `\n`→` ` 后 [:80] 截断、空 → `-`;agent_id 空 → `-`;created_at 经 age_text。
146
+ pub(crate) fn format_latest_result(value: &Value) -> String {
147
+ let agent = non_empty_str(
148
+ value
149
+ .get("agent_id")
150
+ .and_then(Value::as_str)
151
+ ,
152
+ "-",
153
+ );
154
+ let raw_summary = value
155
+ .get("summary")
156
+ .and_then(Value::as_str)
157
+ .unwrap_or("");
158
+ let summary_flat = raw_summary.replace('\n', " ");
159
+ let summary = prefix_chars(&summary_flat, 80);
160
+ let summary = if summary.is_empty() {
161
+ "-".to_string()
162
+ } else {
163
+ summary
164
+ };
165
+ let created = age_text(
166
+ value
167
+ .get("created_at")
168
+ .and_then(Value::as_str)
169
+ ,
170
+ );
171
+ format!("{agent} -> {summary} @ {created}")
172
+ }
173
+
174
+ /// `cmd_status` 的 `--json` 分支 CLI **独占**的不变量(`commands.py:99`):
175
+ /// `compact = not (detail is True)`。即 `detail=false ⇒ compact=true`(默认压缩),
176
+ /// `detail=true ⇒ compact=false`(全量)。这是 CLI 在 status 委派之上唯一拥有的字节级映射。
177
+ pub fn status_compact_flag(detail: bool) -> bool {
178
+ !detail
179
+ }