@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,2 @@
1
+ pub mod parsing;
2
+ pub mod runtime_prompts;
@@ -0,0 +1,452 @@
1
+ use serde::{Deserialize, Serialize};
2
+
3
+ use crate::provider::ApprovalKind;
4
+
5
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6
+ pub struct ApprovalPrompt {
7
+ pub agent_id: String,
8
+ pub state: String,
9
+ pub kind: ApprovalKind,
10
+ pub prompt: String,
11
+ pub choices: Vec<String>,
12
+ #[serde(skip_serializing_if = "Option::is_none")]
13
+ pub tool: Option<String>,
14
+ #[serde(skip_serializing_if = "Option::is_none")]
15
+ pub command: Option<String>,
16
+ }
17
+
18
+ impl ApprovalPrompt {
19
+ pub fn to_ordered_value(&self) -> serde_json::Value {
20
+ let mut map = serde_json::Map::new();
21
+ map.insert("agent_id".to_string(), serde_json::json!(self.agent_id));
22
+ map.insert("state".to_string(), serde_json::json!(self.state));
23
+ map.insert("kind".to_string(), serde_json::json!(self.kind));
24
+ if let Some(tool) = &self.tool {
25
+ map.insert("tool".to_string(), serde_json::json!(tool));
26
+ }
27
+ if let Some(command) = &self.command {
28
+ map.insert("command".to_string(), serde_json::json!(command));
29
+ }
30
+ map.insert("prompt".to_string(), serde_json::json!(self.prompt));
31
+ map.insert("choices".to_string(), serde_json::json!(self.choices));
32
+ serde_json::Value::Object(map)
33
+ }
34
+ }
35
+
36
+ pub fn capture_has_approval_prompt(capture: &str) -> bool {
37
+ extract_approval_prompt("agent", capture).is_some()
38
+ }
39
+
40
+ pub fn active_approval_control_index(lines: &[&str]) -> Option<usize> {
41
+ let idx = lines
42
+ .iter()
43
+ .enumerate()
44
+ .rev()
45
+ .find(|(_, line)| is_control_line(&clean_line(line)))
46
+ .map(|(idx, _)| idx)?;
47
+ if lines.iter().skip(idx + 1).any(|line| !clean_line(line).is_empty()) {
48
+ return None;
49
+ }
50
+ Some(idx)
51
+ }
52
+
53
+ pub fn extract_approval_prompt(agent_id: &str, capture: &str) -> Option<ApprovalPrompt> {
54
+ let lines: Vec<&str> = capture.lines().collect();
55
+ let control_idx = active_approval_control_index(&lines)?;
56
+
57
+ if let Some((matched_idx, prompt, tool)) = explicit_mcp_prompt(&lines, control_idx) {
58
+ let choices = approval_choices_between(&lines, matched_idx, control_idx);
59
+ return Some(approval(agent_id, ApprovalKind::McpTool, prompt, choices, Some(tool), None));
60
+ }
61
+ if let Some((matched_idx, prompt, tool)) = short_mcp_prompt(&lines, control_idx) {
62
+ let choices = approval_choices_between(&lines, matched_idx, control_idx);
63
+ return Some(approval(agent_id, ApprovalKind::McpTool, prompt, choices, Some(tool), None));
64
+ }
65
+ if let Some((matched_idx, prompt, command)) = command_prompt(&lines, control_idx) {
66
+ let choices = approval_choices_between(&lines, matched_idx, control_idx);
67
+ return Some(approval(agent_id, ApprovalKind::Command, prompt, choices, None, Some(command)));
68
+ }
69
+ let choices = approval_choices_between(&lines, 0, control_idx);
70
+ Some(approval(
71
+ agent_id,
72
+ ApprovalKind::Unknown,
73
+ "approval prompt detected".to_string(),
74
+ choices,
75
+ None,
76
+ None,
77
+ ))
78
+ }
79
+
80
+ pub fn choose_internal_mcp_approval_choice(prompt: &ApprovalPrompt) -> String {
81
+ if prompt.choices.iter().any(|choice| choice == "Allow for this session") {
82
+ return "Allow for this session".to_string();
83
+ }
84
+ for choice in &prompt.choices {
85
+ if choice.starts_with("Yes, and don't ask again") {
86
+ return choice.clone();
87
+ }
88
+ }
89
+ for preferred in ["Allow", "Yes"] {
90
+ if prompt.choices.iter().any(|choice| choice == preferred) {
91
+ return preferred.to_string();
92
+ }
93
+ }
94
+ "Allow for this session".to_string()
95
+ }
96
+
97
+ pub fn approval_choice_keys(prompt: &ApprovalPrompt, capture: &str, target_choice: &str) -> Vec<String> {
98
+ let Some(target_index) = prompt.choices.iter().position(|choice| choice == target_choice) else {
99
+ return vec!["Down".to_string(), "Enter".to_string()];
100
+ };
101
+ let Some(active) = active_choice_index(capture) else {
102
+ return vec![(target_index + 1).to_string(), "Enter".to_string()];
103
+ };
104
+ if active == target_index {
105
+ return vec!["Enter".to_string()];
106
+ }
107
+ let key = if target_index < active { "Up" } else { "Down" };
108
+ let count = active.abs_diff(target_index);
109
+ let mut keys = Vec::with_capacity(count + 1);
110
+ keys.extend(std::iter::repeat_n(key.to_string(), count));
111
+ keys.push("Enter".to_string());
112
+ keys
113
+ }
114
+
115
+ fn approval(
116
+ agent_id: &str,
117
+ kind: ApprovalKind,
118
+ prompt: String,
119
+ choices: Vec<String>,
120
+ tool: Option<String>,
121
+ command: Option<String>,
122
+ ) -> ApprovalPrompt {
123
+ ApprovalPrompt {
124
+ agent_id: agent_id.to_string(),
125
+ state: "waiting_approval".to_string(),
126
+ kind,
127
+ prompt,
128
+ choices,
129
+ tool,
130
+ command,
131
+ }
132
+ }
133
+
134
+ fn is_control_line(line: &str) -> bool {
135
+ let lower = line.to_ascii_lowercase();
136
+ lower.contains("enter to submit | esc to cancel")
137
+ || (lower.contains("esc to cancel") && lower.contains("tab to amend"))
138
+ }
139
+
140
+ fn explicit_mcp_prompt(lines: &[&str], control_idx: usize) -> Option<(usize, String, String)> {
141
+ for (idx, raw) in lines.iter().enumerate().take(control_idx).rev() {
142
+ let line = clean_line(raw);
143
+ let prefix = "Allow the team_orchestrator MCP server to run tool \"";
144
+ if let Some(rest) = line.strip_prefix(prefix) {
145
+ if let Some((tool, _)) = rest.split_once('"') {
146
+ if !tool.is_empty() {
147
+ let tool = tool.to_string();
148
+ return Some((idx, line, tool));
149
+ }
150
+ }
151
+ }
152
+ }
153
+ None
154
+ }
155
+
156
+ fn short_mcp_prompt(lines: &[&str], control_idx: usize) -> Option<(usize, String, String)> {
157
+ for (idx, raw) in lines.iter().enumerate().take(control_idx).rev() {
158
+ let line = clean_line(raw);
159
+ if parse_choice_line(&line).is_some() {
160
+ continue;
161
+ }
162
+ if let Some(tool) = team_orchestrator_tool(&line, '-') {
163
+ return Some((idx, format!("team_orchestrator - {tool}"), tool));
164
+ }
165
+ if let Some(tool) = team_orchestrator_tool(&line, '.') {
166
+ return Some((idx, format!("team_orchestrator - {tool}"), tool));
167
+ }
168
+ }
169
+ None
170
+ }
171
+
172
+ fn team_orchestrator_tool(line: &str, separator: char) -> Option<String> {
173
+ let marker = "team_orchestrator";
174
+ let start = line.find(marker)?;
175
+ if start > 0 && line[..start].chars().next_back().is_some_and(is_word_char) {
176
+ return None;
177
+ }
178
+ let rest = &line[start + marker.len()..];
179
+ let rest = rest.trim_start().strip_prefix(separator)?.trim_start();
180
+ let tool: String = rest
181
+ .chars()
182
+ .take_while(|ch| ch.is_ascii_alphanumeric() || *ch == '_')
183
+ .collect();
184
+ if tool.is_empty() || !tool.chars().next().is_some_and(|ch| ch.is_ascii_alphabetic() || ch == '_') {
185
+ None
186
+ } else {
187
+ Some(tool)
188
+ }
189
+ }
190
+
191
+ fn is_word_char(ch: char) -> bool {
192
+ ch.is_ascii_alphanumeric() || ch == '_'
193
+ }
194
+
195
+ fn command_prompt(lines: &[&str], control_idx: usize) -> Option<(usize, String, String)> {
196
+ let prompt_idx = lines
197
+ .iter()
198
+ .enumerate()
199
+ .take(control_idx)
200
+ .rev()
201
+ .find(|(_, line)| clean_line(line) == "Would you like to run the following command?")
202
+ .map(|(idx, _)| idx)?;
203
+ let prompt = clean_line(lines.get(prompt_idx)?);
204
+ let before = lines[..prompt_idx].iter().rev();
205
+ let after = lines.iter().take(control_idx).skip(prompt_idx + 1);
206
+ for line in before.chain(after) {
207
+ let cleaned = clean_line(line);
208
+ if is_command_line(&cleaned) {
209
+ return Some((prompt_idx, prompt, cap_chars(&cleaned, 200)));
210
+ }
211
+ }
212
+ None
213
+ }
214
+
215
+ fn is_command_line(line: &str) -> bool {
216
+ line.starts_with("Bash(") || line.starts_with("Shell(")
217
+ }
218
+
219
+ fn approval_choices_between(lines: &[&str], matched_idx: usize, control_idx: usize) -> Vec<String> {
220
+ let mut choices = Vec::new();
221
+ for line in lines.iter().take(control_idx).skip(matched_idx) {
222
+ if let Some((_, choice)) = parse_choice_line(&clean_line(line)) {
223
+ if !choices.iter().any(|existing| existing == &choice) {
224
+ choices.push(choice);
225
+ }
226
+ }
227
+ }
228
+ choices
229
+ }
230
+
231
+ fn active_choice_index(capture: &str) -> Option<usize> {
232
+ capture.lines().find_map(|line| {
233
+ let cleaned = clean_line(line);
234
+ if !cleaned.starts_with('›') && !cleaned.starts_with('❯') && !cleaned.starts_with('>') {
235
+ return None;
236
+ }
237
+ parse_choice_line(&cleaned).map(|(index, _)| index)
238
+ })
239
+ }
240
+
241
+ fn parse_choice_line(line: &str) -> Option<(usize, String)> {
242
+ let trimmed = line.trim_start_matches(['›', '❯', '>', '*', ' ']);
243
+ let (num, rest) = trimmed.split_once('.')?;
244
+ let raw_index = num.trim().parse::<usize>().ok()?;
245
+ let index = raw_index.checked_sub(1)?;
246
+ let text = trim_choice_columns(rest.trim());
247
+ if text.is_empty() {
248
+ return None;
249
+ }
250
+ Some((index, text.to_string()))
251
+ }
252
+
253
+ fn trim_choice_columns(text: &str) -> &str {
254
+ let bytes = text.as_bytes();
255
+ for idx in 1..bytes.len() {
256
+ if bytes[idx - 1] == b' ' && bytes[idx] == b' ' {
257
+ return text[..idx - 1].trim_end();
258
+ }
259
+ }
260
+ text
261
+ }
262
+
263
+ fn clean_line(line: &str) -> String {
264
+ line.trim()
265
+ .trim_start_matches(['│', '|'])
266
+ .trim()
267
+ .to_string()
268
+ }
269
+
270
+ fn cap_chars(text: &str, max: usize) -> String {
271
+ text.chars().take(max).collect()
272
+ }
273
+
274
+ #[cfg(test)]
275
+ mod tests {
276
+ #![allow(clippy::unwrap_used)]
277
+
278
+ use super::*;
279
+
280
+ #[test]
281
+ fn explicit_mcp_prompt_extracts_tool() {
282
+ let prompt = extract_approval_prompt(
283
+ "agent-x",
284
+ "noise\nAllow the team_orchestrator MCP server to run tool \"send_message\"?\n 1. Allow for this session\n❯ 2. Deny\nEnter to submit | Esc to cancel\n",
285
+ )
286
+ .unwrap();
287
+ assert_eq!(prompt.agent_id, "agent-x");
288
+ assert_eq!(prompt.state, "waiting_approval");
289
+ assert_eq!(prompt.kind, ApprovalKind::McpTool);
290
+ assert_eq!(prompt.tool.as_deref(), Some("send_message"));
291
+ assert_eq!(prompt.prompt, "Allow the team_orchestrator MCP server to run tool \"send_message\"?");
292
+ assert_eq!(prompt.choices, vec!["Allow for this session", "Deny"]);
293
+ }
294
+
295
+ #[test]
296
+ fn short_mcp_prompt_extracts_bare_tool() {
297
+ let prompt = extract_approval_prompt(
298
+ "agent-x",
299
+ "team_orchestrator - assign_task\n 1. Yes\nEnter to submit | Esc to cancel\n",
300
+ )
301
+ .unwrap();
302
+ assert_eq!(prompt.kind, ApprovalKind::McpTool);
303
+ assert_eq!(prompt.tool.as_deref(), Some("assign_task"));
304
+ assert_eq!(prompt.prompt, "team_orchestrator - assign_task");
305
+ }
306
+
307
+ #[test]
308
+ fn command_prompt_extracts_command_before_prompt() {
309
+ let prompt = extract_approval_prompt(
310
+ "agent-x",
311
+ "Bash(echo hi)\nWould you like to run the following command?\n 1. Yes\nEnter to submit | Esc to cancel\n",
312
+ )
313
+ .unwrap();
314
+ assert_eq!(prompt.kind, ApprovalKind::Command);
315
+ assert_eq!(prompt.command.as_deref(), Some("Bash(echo hi)"));
316
+ assert_eq!(prompt.prompt, "Would you like to run the following command?");
317
+ }
318
+
319
+ #[test]
320
+ fn control_followed_by_non_prompt_text_is_none() {
321
+ assert_eq!(
322
+ active_approval_control_index(&"team_orchestrator - send_message\nEnter to submit | Esc to cancel\nlater output\n".lines().collect::<Vec<_>>()),
323
+ None
324
+ );
325
+ assert_eq!(
326
+ extract_approval_prompt("agent-x", "MCP tool: team_orchestrator - send_message requires approval\n 1. Allow\n"),
327
+ None
328
+ );
329
+ }
330
+
331
+ #[test]
332
+ fn unknown_fallback_has_choices_and_state() {
333
+ let prompt = extract_approval_prompt(
334
+ "agent-x",
335
+ "random approval text\n 1. Allow\nEnter to submit | Esc to cancel\n",
336
+ )
337
+ .unwrap();
338
+ assert_eq!(prompt.kind, ApprovalKind::Unknown);
339
+ assert_eq!(prompt.prompt, "approval prompt detected");
340
+ assert_eq!(prompt.choices, vec!["Allow"]);
341
+ }
342
+
343
+ #[test]
344
+ fn choose_internal_mcp_approval_choice_matches_golden() {
345
+ let mut prompt = approval(
346
+ "agent-x",
347
+ ApprovalKind::McpTool,
348
+ String::new(),
349
+ vec![
350
+ "Allow for this session".to_string(),
351
+ "Yes, and don't ask again".to_string(),
352
+ "Allow".to_string(),
353
+ "Yes".to_string(),
354
+ ],
355
+ None,
356
+ None,
357
+ );
358
+ assert_eq!(choose_internal_mcp_approval_choice(&prompt), "Allow for this session");
359
+ prompt.choices = vec!["Maybe".to_string()];
360
+ assert_eq!(choose_internal_mcp_approval_choice(&prompt), "Allow for this session");
361
+ prompt.choices = vec!["Maybe".to_string(), "Yes, and don't ask again for this command".to_string(), "Allow".to_string()];
362
+ assert_eq!(
363
+ choose_internal_mcp_approval_choice(&prompt),
364
+ "Yes, and don't ask again for this command"
365
+ );
366
+ }
367
+
368
+ #[test]
369
+ fn approval_choice_keys_match_golden() {
370
+ let prompt = approval(
371
+ "agent-x",
372
+ ApprovalKind::McpTool,
373
+ String::new(),
374
+ vec!["Allow".to_string(), "Deny".to_string()],
375
+ None,
376
+ None,
377
+ );
378
+ assert_eq!(approval_choice_keys(&prompt, "❯ 1. Allow\n", "Missing"), vec!["Down", "Enter"]);
379
+ assert_eq!(approval_choice_keys(&prompt, " 1. Allow\n 2. Deny\n", "Deny"), vec!["2", "Enter"]);
380
+ assert_eq!(approval_choice_keys(&prompt, "❯ 1. Allow\n 2. Deny\n", "Deny"), vec!["Down", "Enter"]);
381
+ assert_eq!(approval_choice_keys(&prompt, " 1. Allow\n❯ 2. Deny\n", "Allow"), vec!["Up", "Enter"]);
382
+ assert_eq!(approval_choice_keys(&prompt, "❯ 1. Allow\n 2. Deny\n", "Allow"), vec!["Enter"]);
383
+ }
384
+
385
+ #[test]
386
+ fn m5r2_boundary_cases_match_golden_review() {
387
+ let mcp = extract_approval_prompt(
388
+ "agent-x",
389
+ "Allow the team_orchestrator MCP server to run tool \"send_message\"?\nnoise1\nnoise2\nnoise3\nnoise4\nnoise5\nnoise6\nnoise7\n 1. Allow\nenter to submit | esc to cancel\n",
390
+ )
391
+ .unwrap();
392
+ assert_eq!(mcp.tool.as_deref(), Some("send_message"));
393
+
394
+ let unknown = extract_approval_prompt(
395
+ "agent-x",
396
+ "xteam_orchestrator - assign_task\n 1. Yes\nEnter to submit | Esc to cancel\n",
397
+ )
398
+ .unwrap();
399
+ assert_eq!(unknown.kind, ApprovalKind::Unknown);
400
+
401
+ let command = extract_approval_prompt(
402
+ "agent-x",
403
+ "Bash(echo hi)\nnoise1\nnoise2\nnoise3\nnoise4\nnoise5\nnoise6\nnoise7\nWould you like to run the following command?\n 1. Yes\nEnter to submit | Esc to cancel\n",
404
+ )
405
+ .unwrap();
406
+ assert_eq!(command.command.as_deref(), Some("Bash(echo hi)"));
407
+ }
408
+
409
+ #[test]
410
+ fn ordered_value_places_tool_or_command_before_prompt() {
411
+ let mcp = approval(
412
+ "agent-x",
413
+ ApprovalKind::McpTool,
414
+ "p".to_string(),
415
+ vec!["Allow".to_string()],
416
+ Some("send_message".to_string()),
417
+ None,
418
+ )
419
+ .to_ordered_value();
420
+ let keys: Vec<&str> = mcp.as_object().unwrap().keys().map(String::as_str).collect();
421
+ assert_eq!(keys, vec!["agent_id", "state", "kind", "tool", "prompt", "choices"]);
422
+
423
+ let command = approval(
424
+ "agent-x",
425
+ ApprovalKind::Command,
426
+ "p".to_string(),
427
+ vec!["Yes".to_string()],
428
+ None,
429
+ Some("Bash(echo hi)".to_string()),
430
+ )
431
+ .to_ordered_value();
432
+ let keys: Vec<&str> = command.as_object().unwrap().keys().map(String::as_str).collect();
433
+ assert_eq!(keys, vec!["agent_id", "state", "kind", "command", "prompt", "choices"]);
434
+ }
435
+
436
+ #[test]
437
+ fn choices_are_scoped_to_matched_prompt() {
438
+ let mcp = extract_approval_prompt(
439
+ "agent-x",
440
+ " 1. Old Allow\nold text\nteam_orchestrator - send_message\n 1. Yes\nEnter to submit | Esc to cancel\n",
441
+ )
442
+ .unwrap();
443
+ assert_eq!(mcp.choices, vec!["Yes"]);
444
+
445
+ let command = extract_approval_prompt(
446
+ "agent-x",
447
+ " 1. Old Allow\nBash(echo hi)\nWould you like to run the following command?\n 1. Yes\nEnter to submit | Esc to cancel\n",
448
+ )
449
+ .unwrap();
450
+ assert_eq!(command.choices, vec!["Yes"]);
451
+ }
452
+ }
@@ -0,0 +1,163 @@
1
+ //! Runtime approval prompt decisions shared by coordinator hooks.
2
+
3
+ use serde::{Deserialize, Serialize};
4
+ use sha2::{Digest, Sha256};
5
+
6
+ use crate::provider::{ApprovalFingerprint, ApprovalKind, ApprovalPrompt};
7
+
8
+ pub const RUNTIME_MCP_APPROVAL_ALLOWLIST: &[&str] = &[
9
+ "send_message",
10
+ "report_result",
11
+ "get_team_status",
12
+ "request_human",
13
+ ];
14
+
15
+ pub fn runtime_mcp_tool_allowlisted(tool: &str) -> bool {
16
+ RUNTIME_MCP_APPROVAL_ALLOWLIST.contains(&tool)
17
+ }
18
+
19
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20
+ #[serde(rename_all = "snake_case")]
21
+ pub enum RuntimeApprovalDecision {
22
+ AutoApprove,
23
+ AwaitingHumanConfirm,
24
+ Ignore,
25
+ }
26
+
27
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28
+ pub struct AwaitingHumanConfirmFact {
29
+ pub team: String,
30
+ pub agent_id: String,
31
+ pub fingerprint: ApprovalFingerprint,
32
+ pub dedupe_key: String,
33
+ pub prompt_kind: String,
34
+ pub prompt: String,
35
+ pub tool: Option<String>,
36
+ pub command: Option<String>,
37
+ pub reason: String,
38
+ pub next_step: String,
39
+ }
40
+
41
+ impl AwaitingHumanConfirmFact {
42
+ pub fn to_event_payload(&self) -> serde_json::Value {
43
+ serde_json::json!({
44
+ "event": "worker.awaiting_human_confirm",
45
+ "team": self.team,
46
+ "team_id": self.team,
47
+ "owner_team_id": self.team,
48
+ "agent_id": self.agent_id,
49
+ "fingerprint": self.fingerprint.as_str(),
50
+ "dedupe_key": self.dedupe_key,
51
+ "prompt_kind": self.prompt_kind,
52
+ "prompt": self.prompt,
53
+ "tool": self.tool,
54
+ "command": self.command,
55
+ "reason": self.reason,
56
+ "next_step": self.next_step,
57
+ })
58
+ }
59
+
60
+ pub fn to_leader_message_content(&self) -> String {
61
+ self.to_event_payload().to_string()
62
+ }
63
+ }
64
+
65
+ pub fn runtime_approval_decision(
66
+ prompt: &ApprovalPrompt,
67
+ leader_auto_approval_allowed: bool,
68
+ ) -> RuntimeApprovalDecision {
69
+ match awaiting_human_confirm_reason(prompt, leader_auto_approval_allowed) {
70
+ Some(_) => RuntimeApprovalDecision::AwaitingHumanConfirm,
71
+ None if prompt.kind == ApprovalKind::McpTool => RuntimeApprovalDecision::AutoApprove,
72
+ None => RuntimeApprovalDecision::Ignore,
73
+ }
74
+ }
75
+
76
+ pub fn awaiting_human_confirm_reason(
77
+ prompt: &ApprovalPrompt,
78
+ leader_auto_approval_allowed: bool,
79
+ ) -> Option<&'static str> {
80
+ match prompt.kind {
81
+ ApprovalKind::McpTool => {
82
+ let tool = prompt.tool.as_deref()?;
83
+ if !runtime_mcp_tool_allowlisted(tool) {
84
+ Some("tool_not_allowlisted")
85
+ } else if !leader_auto_approval_allowed {
86
+ Some("leader_restricted")
87
+ } else {
88
+ None
89
+ }
90
+ }
91
+ ApprovalKind::Command => {
92
+ if leader_auto_approval_allowed {
93
+ None
94
+ } else {
95
+ Some("leader_restricted")
96
+ }
97
+ }
98
+ ApprovalKind::Unknown => Some("approval_requires_human"),
99
+ }
100
+ }
101
+
102
+ pub fn approval_prompt_fingerprint(team: &str, agent_id: &str, prompt: &ApprovalPrompt) -> ApprovalFingerprint {
103
+ let mut hasher = Sha256::new();
104
+ hasher.update(team.as_bytes());
105
+ hasher.update([0]);
106
+ hasher.update(agent_id.as_bytes());
107
+ hasher.update([0]);
108
+ hasher.update(prompt.prompt.as_bytes());
109
+ hasher.update([0]);
110
+ if let Some(tool) = prompt.tool.as_deref() {
111
+ hasher.update(tool.as_bytes());
112
+ }
113
+ hasher.update([0]);
114
+ if let Some(command) = prompt.command.as_deref() {
115
+ hasher.update(command.as_bytes());
116
+ }
117
+ hasher.update([0]);
118
+ hasher.update(format!("{:?}", prompt.kind).as_bytes());
119
+ let digest = hasher.finalize();
120
+ let fingerprint = digest
121
+ .iter()
122
+ .take(8)
123
+ .map(|byte| format!("{byte:02x}"))
124
+ .collect::<String>();
125
+ ApprovalFingerprint::new(fingerprint)
126
+ }
127
+
128
+ pub fn awaiting_human_confirm_dedupe_key(
129
+ team: &str,
130
+ agent_id: &str,
131
+ fingerprint: &ApprovalFingerprint,
132
+ ) -> String {
133
+ format!("awaiting_human_confirm:{team}:{agent_id}:{}", fingerprint.as_str())
134
+ }
135
+
136
+ pub fn awaiting_human_confirm_fact(
137
+ team: &str,
138
+ agent_id: &str,
139
+ prompt: &ApprovalPrompt,
140
+ reason: &str,
141
+ ) -> AwaitingHumanConfirmFact {
142
+ let fingerprint = approval_prompt_fingerprint(team, agent_id, prompt);
143
+ AwaitingHumanConfirmFact {
144
+ team: team.to_string(),
145
+ agent_id: agent_id.to_string(),
146
+ dedupe_key: awaiting_human_confirm_dedupe_key(team, agent_id, &fingerprint),
147
+ fingerprint,
148
+ prompt_kind: prompt_kind_wire(prompt.kind).to_string(),
149
+ prompt: prompt.prompt.clone(),
150
+ tool: prompt.tool.clone(),
151
+ command: prompt.command.clone(),
152
+ reason: reason.to_string(),
153
+ next_step: "review the worker pane and approve or deny the prompt manually".to_string(),
154
+ }
155
+ }
156
+
157
+ fn prompt_kind_wire(kind: ApprovalKind) -> &'static str {
158
+ match kind {
159
+ ApprovalKind::McpTool => "mcp_tool",
160
+ ApprovalKind::Command => "command",
161
+ ApprovalKind::Unknown => "unknown",
162
+ }
163
+ }