@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
@@ -1,186 +0,0 @@
1
- """Legacy reverse-scan tmux helpers retained for compatibility with
2
- existing receiver-discovery / takeover / claim-leader fallback paths.
3
-
4
- 0.2.6 main slice (Family A) introduced
5
- ``team_agent.leader_binding.bind_owner_from_caller_pane`` as the positive
6
- source for owner identity. These helpers remain available for older
7
- code paths and tests; they live in a non-linted module so that the
8
- positive-source CI lint (C24) can succeed on the files where the
9
- contract bans reverse enumeration patterns.
10
- """
11
-
12
- from __future__ import annotations
13
-
14
- import os
15
- from pathlib import Path
16
- from typing import Any
17
-
18
- from team_agent.runtime import TMUX_PANE_FORMAT, run_cmd
19
-
20
-
21
- def _tmux_current_client_pane_info() -> dict[str, str] | None:
22
- proc = run_cmd(["tmux", "display-message", "-p", "-F", TMUX_PANE_FORMAT], timeout=5)
23
- if proc.returncode != 0:
24
- return None
25
- return _parse_tmux_pane_info(proc.stdout.strip())
26
-
27
-
28
- def _tmux_list_panes() -> list[dict[str, str]]:
29
- proc = run_cmd(["tmux", "list-panes", "-a", "-F", TMUX_PANE_FORMAT], timeout=5)
30
- if proc.returncode != 0:
31
- return []
32
- return [pane for line in proc.stdout.splitlines() if (pane := _parse_tmux_pane_info(line))]
33
-
34
-
35
- def _tmux_pane_info(target: str | None) -> dict[str, str] | None:
36
- if not target:
37
- return None
38
- proc = run_cmd(["tmux", "display-message", "-p", "-t", target, "-F", TMUX_PANE_FORMAT], timeout=5)
39
- if proc.returncode != 0:
40
- return None
41
- return _parse_tmux_pane_info(proc.stdout.strip())
42
-
43
-
44
- def _parse_tmux_pane_info(line: str) -> dict[str, str] | None:
45
- parts = line.split("\t")
46
- if len(parts) not in {8, 10, 11}:
47
- return None
48
- keys = [
49
- "pane_id",
50
- "session_name",
51
- "window_index",
52
- "window_name",
53
- "pane_index",
54
- "pane_tty",
55
- "pane_current_command",
56
- "pane_active",
57
- ]
58
- if len(parts) >= 10:
59
- keys.extend(["pane_current_path", "session_attached"])
60
- if len(parts) == 11:
61
- keys.append("pane_in_mode")
62
- return dict(zip(keys, parts))
63
-
64
-
65
- def _infer_active_tmux_pane(provider: str) -> dict[str, str] | None:
66
- from team_agent.messaging.leader_panes import _leader_command_looks_usable
67
- panes = _tmux_list_panes()
68
- active = [pane for pane in panes if pane.get("pane_active") == "1"]
69
- preferred = [pane for pane in active if _leader_command_looks_usable(pane.get("pane_current_command", ""), provider)]
70
- if len(preferred) == 1:
71
- return preferred[0]
72
- if len(active) == 1:
73
- return active[0]
74
- if preferred:
75
- return preferred[0]
76
- return active[0] if active else None
77
-
78
-
79
- def _infer_workspace_tmux_pane(provider: str, workspace: Path) -> dict[str, Any]:
80
- from team_agent.messaging.leader_panes import (
81
- _leader_command_looks_usable,
82
- _leader_command_provider,
83
- )
84
- panes = _tmux_list_panes()
85
- workspace_panes = [pane for pane in panes if _pane_path_matches_workspace(pane, workspace)]
86
- candidates = [
87
- pane
88
- for pane in workspace_panes
89
- if _leader_command_looks_usable(pane.get("pane_current_command", ""), provider)
90
- or _leader_command_provider(pane.get("pane_current_command", "")) is not None
91
- ]
92
- if not candidates:
93
- return {"status": "missing", "workspace_panes": workspace_panes}
94
- ranked = sorted(candidates, key=lambda item: _leader_pane_rank(item, provider), reverse=True)
95
- best_rank = _leader_pane_rank(ranked[0], provider)
96
- best = [pane for pane in ranked if _leader_pane_rank(pane, provider) == best_rank]
97
- if len(best) == 1:
98
- return {"status": "ok", "pane": best[0], "candidates": candidates}
99
- return {"status": "ambiguous", "candidates": best}
100
-
101
-
102
- def _pane_is_usable_leader(pane: dict[str, str], provider: str, workspace: Path | None) -> bool:
103
- _ = provider
104
- if workspace is not None and not _pane_path_matches_workspace(pane, workspace):
105
- return False
106
- return True
107
-
108
-
109
- def _pane_path_matches_workspace(pane: dict[str, str], workspace: Path) -> bool:
110
- current_path = pane.get("pane_current_path")
111
- if not current_path:
112
- return False
113
- return os.path.realpath(current_path) == os.path.realpath(str(workspace.resolve()))
114
-
115
-
116
- def _leader_pane_rank(pane: dict[str, str], provider: str) -> tuple[int, int, int]:
117
- from team_agent.messaging.leader_panes import _leader_command_is_exact
118
- return (
119
- _tmux_truthy(pane.get("session_attached", "")),
120
- 1 if pane.get("pane_active") == "1" else 0,
121
- 1 if _leader_command_is_exact(pane.get("pane_current_command", ""), provider) else 0,
122
- )
123
-
124
-
125
- def _tmux_truthy(value: str) -> int:
126
- try:
127
- return 1 if int(value) > 0 else 0
128
- except (TypeError, ValueError):
129
- return 1 if value and value != "0" else 0
130
-
131
-
132
- def _format_leader_pane_candidates(candidates: list[dict[str, str]]) -> str:
133
- compact = []
134
- for pane in candidates[:5]:
135
- compact.append(
136
- "{pane_id} session={session_name} pane={window_index}.{pane_index} "
137
- "cmd={pane_current_command} cwd={pane_current_path} active={pane_active}".format(**pane)
138
- )
139
- suffix = "" if len(candidates) <= 5 else f" ... +{len(candidates) - 5} more"
140
- return "candidates: " + "; ".join(compact) + suffix
141
-
142
-
143
- def _resolve_leader_pane(
144
- pane: str | None,
145
- provider: str,
146
- workspace: Path | None = None,
147
- require_current: bool = False,
148
- ) -> tuple[dict[str, str], str]:
149
- from team_agent.errors import RuntimeError as _RuntimeError
150
- if pane:
151
- pane_info = _tmux_pane_info(pane)
152
- if not pane_info:
153
- raise _RuntimeError(f"tmux pane not found: {pane}")
154
- return pane_info, "explicit_pane"
155
- pane_info = _tmux_current_client_pane_info()
156
- if pane_info and _pane_is_usable_leader(pane_info, provider, workspace):
157
- return pane_info, "current_client"
158
- if workspace is not None:
159
- workspace_match = _infer_workspace_tmux_pane(provider, workspace)
160
- if workspace_match["status"] == "ok":
161
- return workspace_match["pane"], "workspace_pane_scan"
162
- if workspace_match["status"] == "ambiguous":
163
- raise _RuntimeError(
164
- "multiple tmux leader panes match this workspace; pass --pane explicitly. "
165
- + _format_leader_pane_candidates(workspace_match["candidates"])
166
- )
167
- if require_current:
168
- details = ""
169
- if pane_info:
170
- details = (
171
- f" Current tmux client points at pane {pane_info.get('pane_id')} "
172
- f"command={pane_info.get('pane_current_command')!r} "
173
- f"cwd={pane_info.get('pane_current_path')!r}, not a usable pane for this workspace."
174
- )
175
- raise _RuntimeError(
176
- "Team Agent could not locate a tmux-managed leader pane for this workspace. "
177
- "Run quick-start from the visible tmux-managed leader pane, "
178
- "or use `team-agent codex`/`team-agent claude` as a convenience fallback."
179
- + details
180
- )
181
- if pane_info and workspace is None:
182
- return pane_info, "current_client"
183
- pane_info = _infer_active_tmux_pane(provider)
184
- if pane_info:
185
- return pane_info, "active_pane_scan"
186
- raise _RuntimeError("could not infer a tmux leader pane; pass --pane <pane_id>")
@@ -1,253 +0,0 @@
1
- """Provider-neutral abnormal-state track (Gap 32 §4).
2
-
3
- Reads structured fault records + process identity; never reads a screen and
4
- never names a provider. Catch-bias for structured error/failed-class records
5
- (C9), dedup by (signature, turn) (C8), and coordinator-independent whole-team
6
- disappearance with clean-shutdown vs unexpected distinction (C10).
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- from typing import Any
12
-
13
-
14
- def process_abnormal_records(
15
- records: list[dict[str, Any]],
16
- *,
17
- registry: Any,
18
- notification_state: dict[str, Any] | None,
19
- event_sink: Any = None,
20
- ) -> dict[str, Any]:
21
- """Classify raw provider session records that may carry faults.
22
-
23
- ``registry`` carries the provider whose records these are (``{"provider":
24
- name}``) or a full registry mapping. Records are turned into structured
25
- fault facts by the provider reader (so this module names no provider), then
26
- catch-biased + deduped by (signature, turn).
27
- """
28
- from team_agent.provider_state import read_fault_facts
29
- from team_agent.provider_state.registry import get_provider_registry
30
-
31
- state = dict(notification_state or {})
32
- seen = set(state.get("seen") or [])
33
- notifications: list[dict[str, Any]] = []
34
- discovery_log: list[dict[str, Any]] = []
35
- diagnostics: list[dict[str, Any]] = []
36
-
37
- provider = _provider_of(registry)
38
- white, black = _lists_for(provider, registry, get_provider_registry)
39
-
40
- faults = read_fault_facts(provider, records or []) if provider else []
41
- if not faults and records:
42
- # Records that produced no structured fault fact are not default-notify
43
- # candidates (C9): arbitrary unrecognized lines become diagnostics only.
44
- diagnostics.append({"kind": "no_structured_fault", "count": len(records)})
45
-
46
- for fact in faults:
47
- signature = str(fact.get("signature") or fact.get("reason") or "fault")
48
- turn_id = fact.get("turn_id")
49
- text = " ".join(str(x) for x in (signature, fact.get("reason"), _raw_message(fact)) if x).lower()
50
- decision = _classify(text, signature, white, black)
51
- discovery_log.append({
52
- "signature": signature,
53
- "turn_id": turn_id,
54
- "decision": decision,
55
- "kind": fact.get("kind"),
56
- "provider": provider,
57
- })
58
- if decision == "skip":
59
- continue
60
- # C8: dedup by (signature, turn_id) — a retry loop in the SAME turn folds
61
- # to one notify. But a MISSING turn_id must not collapse distinct errors
62
- # into one global bucket: discriminate by a per-record content fingerprint
63
- # so genuinely different faults each notify (identical duplicates still fold).
64
- bucket = turn_id if turn_id is not None else f"norow:{_record_fingerprint(fact)}"
65
- dedupe_key = (signature, bucket)
66
- key = f"{signature}\x00{bucket}"
67
- if key in seen:
68
- continue
69
- seen.add(key)
70
- notifications.append({
71
- "signature": signature,
72
- "turn_id": turn_id,
73
- "dedupe_key": dedupe_key,
74
- "state": "blocked_on_human" if fact.get("kind") == "approval" else "abnormal",
75
- "decision": decision,
76
- "provider": provider,
77
- "raw": fact.get("raw", fact),
78
- "raw_record": fact.get("raw", fact),
79
- })
80
- _emit(event_sink, "abnormal.notify", signature=signature, turn_id=turn_id, decision=decision)
81
-
82
- state["seen"] = sorted(seen)
83
- return {
84
- "notifications": notifications,
85
- "discovery_log": discovery_log,
86
- "diagnostics": diagnostics,
87
- "notification_state": state,
88
- }
89
-
90
-
91
- def detect_whole_team_gone(
92
- snapshot: dict[str, Any],
93
- *,
94
- marker_store: Any,
95
- event_sink: Any = None,
96
- ) -> dict[str, Any]:
97
- """Coordinator-independent whole-team-gone detection (C10/C13).
98
-
99
- Does not require the coordinator to be alive. The whole team is gone when the
100
- coordinator, the leader, every provider process, and every session are all
101
- absent. Clean shutdown / restart-in-progress (flagged in the snapshot) are
102
- silent; an unexpected disappearance records a durable marker and defers user
103
- escalation to the next leader command.
104
- """
105
- coordinator = snapshot.get("coordinator") or {}
106
- leader = snapshot.get("leader") or {}
107
- provider_processes = snapshot.get("provider_processes")
108
- if provider_processes is None:
109
- provider_processes = snapshot.get("nodes") or snapshot.get("agents") or []
110
- tmux_sessions = snapshot.get("tmux_sessions") or []
111
-
112
- coord_alive = _alive(coordinator)
113
- leader_alive = _alive(leader)
114
- any_worker_alive = any(_alive(p) for p in provider_processes)
115
- sessions_present = bool(tmux_sessions)
116
-
117
- whole_gone = not (coord_alive or leader_alive or any_worker_alive or sessions_present)
118
-
119
- if not whole_gone:
120
- return {
121
- "state": "alive",
122
- "whole_team_gone": False,
123
- "classification": "alive",
124
- "notify": False,
125
- "escalate_user_on_next_leader_command": False,
126
- "marker_written": False,
127
- }
128
-
129
- if snapshot.get("clean_shutdown"):
130
- return _silent_gone("clean_shutdown")
131
- if snapshot.get("restart_in_progress"):
132
- return _silent_gone("restart_in_progress")
133
-
134
- # Unexpected disappearance (闪退): durable marker + deferred escalation.
135
- marker_written = _marker_set(marker_store, "whole_team_gone", {
136
- "classification": "unexpected_exit",
137
- "provider_processes": len(provider_processes),
138
- })
139
- _emit(event_sink, "abnormal.whole_team_gone", classification="unexpected_exit")
140
- return {
141
- "state": "whole_team_gone",
142
- "whole_team_gone": True,
143
- "classification": "unexpected_exit",
144
- "notify": True,
145
- "escalate_user_on_next_leader_command": True,
146
- "marker_written": bool(marker_written),
147
- }
148
-
149
-
150
- def _silent_gone(classification: str) -> dict[str, Any]:
151
- return {
152
- "state": classification,
153
- "whole_team_gone": True,
154
- "classification": classification,
155
- "notify": False,
156
- "escalate_user_on_next_leader_command": False,
157
- "marker_written": False,
158
- }
159
-
160
-
161
- def _alive(entry: Any) -> bool:
162
- from team_agent.provider_state.common import process_is_live
163
-
164
- if isinstance(entry, dict):
165
- if "alive" in entry:
166
- return entry.get("alive") is True
167
- if "process" in entry:
168
- ok, _r, _d = process_is_live(entry.get("process"))
169
- return ok
170
- ok, _r, _d = process_is_live(entry)
171
- return ok
172
- return bool(entry)
173
-
174
-
175
- def _provider_of(registry: Any) -> str | None:
176
- if isinstance(registry, dict):
177
- if isinstance(registry.get("provider"), str):
178
- return registry.get("provider")
179
- if isinstance(registry.get("kind"), str):
180
- return registry.get("kind")
181
- return None
182
-
183
-
184
- def _lists_for(provider: str | None, registry: Any, get_provider_registry: Any) -> tuple[list[str], list[str]]:
185
- entry: Any = None
186
- if isinstance(registry, dict) and ("error_whitelist" in registry or "error_blacklist" in registry):
187
- entry = registry
188
- elif provider is not None:
189
- entry = get_provider_registry(provider)
190
- if not isinstance(entry, dict):
191
- return [], []
192
- lists = entry.get("error_lists") if isinstance(entry.get("error_lists"), dict) else {}
193
- white = [str(x).lower() for x in (lists.get("whitelist") or entry.get("error_whitelist") or [])]
194
- black = [str(x).lower() for x in (lists.get("blacklist") or entry.get("error_blacklist") or [])]
195
- return white, black
196
-
197
-
198
- def _classify(text: str, signature: str, white: list[str], black: list[str]) -> str:
199
- sig = signature.lower()
200
- if any(w and (w in text or w in sig) for w in white):
201
- return "skip" # whitelist > blacklist > default
202
- if any(b and (b in text or b in sig) for b in black):
203
- return "notify_blacklist"
204
- return "notify_default" # C9 catch-bias for structured faults
205
-
206
-
207
- def _record_fingerprint(fact: dict[str, Any]) -> str:
208
- import hashlib
209
- import json
210
-
211
- raw = fact.get("raw", fact)
212
- try:
213
- blob = json.dumps(raw, sort_keys=True, default=str)
214
- except (TypeError, ValueError):
215
- blob = repr(raw)
216
- return hashlib.sha256(blob.encode("utf-8", errors="ignore")).hexdigest()[:16]
217
-
218
-
219
- def _raw_message(fact: dict[str, Any]) -> str:
220
- raw = fact.get("raw")
221
- if isinstance(raw, dict):
222
- return str(raw.get("message") or "")
223
- return ""
224
-
225
-
226
- def _marker_set(marker_store: Any, name: str, value: Any) -> bool:
227
- if marker_store is None:
228
- return False
229
- if isinstance(marker_store, dict):
230
- marker_store[name] = value
231
- return True
232
- setter = getattr(marker_store, "set", None) or getattr(marker_store, "write", None)
233
- if callable(setter):
234
- try:
235
- setter(name, value)
236
- return True
237
- except Exception:
238
- return False
239
- return False
240
-
241
-
242
- def _emit(event_sink: Any, name: str, **fields: Any) -> None:
243
- if event_sink is None:
244
- return
245
- try:
246
- event_sink(name, fields)
247
- except TypeError:
248
- try:
249
- event_sink({"event": name, **fields})
250
- except Exception:
251
- pass
252
- except Exception:
253
- pass
@@ -1,65 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from team_agent.approvals.constants import (
4
- INTERNAL_MCP_APPROVAL_CHOICE,
5
- INTERNAL_MCP_AUTO_APPROVE_TOOLS,
6
- STARTUP_PROMPT_RUNTIME_CHECK_LIMIT,
7
- )
8
- from team_agent.approvals.parsing import (
9
- APPROVAL_CHOICE_RE,
10
- active_approval_choice_index,
11
- active_approval_control_index,
12
- approval_choice_keys,
13
- approval_prompt_fingerprint,
14
- capture_has_approval_prompt,
15
- capture_has_team_orchestrator_mcp_prompt,
16
- choose_internal_mcp_approval_choice,
17
- extract_approval_choices,
18
- extract_approval_prompt,
19
- extract_command_approval_subject,
20
- is_approval_control_line,
21
- line_is_approval_choice,
22
- )
23
- from team_agent.approvals.runtime_prompts import (
24
- handle_internal_mcp_approval_prompt,
25
- handle_provider_runtime_prompts,
26
- handle_provider_startup_prompts,
27
- submit_internal_mcp_approval,
28
- )
29
- from team_agent.approvals.status import (
30
- age_text,
31
- agent_health_status,
32
- current_task_for_agent,
33
- detect_provider_status,
34
- refresh_agent_runtime_statuses,
35
- sync_agent_health,
36
- )
37
-
38
- __all__ = [
39
- "APPROVAL_CHOICE_RE",
40
- "INTERNAL_MCP_APPROVAL_CHOICE",
41
- "INTERNAL_MCP_AUTO_APPROVE_TOOLS",
42
- "STARTUP_PROMPT_RUNTIME_CHECK_LIMIT",
43
- "active_approval_choice_index",
44
- "active_approval_control_index",
45
- "age_text",
46
- "agent_health_status",
47
- "approval_choice_keys",
48
- "approval_prompt_fingerprint",
49
- "capture_has_approval_prompt",
50
- "capture_has_team_orchestrator_mcp_prompt",
51
- "choose_internal_mcp_approval_choice",
52
- "current_task_for_agent",
53
- "detect_provider_status",
54
- "extract_approval_choices",
55
- "extract_approval_prompt",
56
- "extract_command_approval_subject",
57
- "handle_internal_mcp_approval_prompt",
58
- "handle_provider_runtime_prompts",
59
- "handle_provider_startup_prompts",
60
- "is_approval_control_line",
61
- "line_is_approval_choice",
62
- "refresh_agent_runtime_statuses",
63
- "submit_internal_mcp_approval",
64
- "sync_agent_health",
65
- ]
@@ -1,6 +0,0 @@
1
- from __future__ import annotations
2
-
3
-
4
- STARTUP_PROMPT_RUNTIME_CHECK_LIMIT = 3
5
- INTERNAL_MCP_AUTO_APPROVE_TOOLS = {"send_message", "report_result", "get_team_status", "request_human"}
6
- INTERNAL_MCP_APPROVAL_CHOICE = "Allow for this session"
@@ -1,176 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import hashlib
4
- import json
5
- import re
6
- from typing import Any
7
-
8
- from team_agent.approvals.constants import INTERNAL_MCP_APPROVAL_CHOICE
9
-
10
-
11
- APPROVAL_CHOICE_RE = re.compile(r"(?:[›❯>]\s*)?(\d+)\.\s+(.+?)(?:\s{2,}.+)?$")
12
-
13
-
14
- def capture_has_approval_prompt(text: str) -> bool:
15
- return extract_approval_prompt("_", text) is not None
16
-
17
-
18
- def extract_approval_prompt(agent_id: str, text: str) -> dict[str, Any] | None:
19
- lines = text.splitlines()
20
- control_index = active_approval_control_index(lines)
21
- if control_index is None:
22
- return None
23
- for index in range(control_index, -1, -1):
24
- line = lines[index]
25
- if "Allow the team_orchestrator MCP server to run tool" not in line:
26
- continue
27
- tool_match = re.search(r'run tool "([^"]+)"', line)
28
- return {
29
- "agent_id": agent_id,
30
- "state": "waiting_approval",
31
- "kind": "mcp_tool",
32
- "tool": tool_match.group(1) if tool_match else None,
33
- "prompt": line.strip(),
34
- "choices": extract_approval_choices(lines[index : control_index + 1]),
35
- }
36
- for index in range(control_index, -1, -1):
37
- line = lines[index]
38
- if line_is_approval_choice(line):
39
- continue
40
- tool_match = re.search(r"\bteam_orchestrator\s*[-.]\s*([A-Za-z_][A-Za-z0-9_]*)\b", line)
41
- if not tool_match:
42
- continue
43
- return {
44
- "agent_id": agent_id,
45
- "state": "waiting_approval",
46
- "kind": "mcp_tool",
47
- "tool": tool_match.group(1),
48
- "prompt": f"team_orchestrator - {tool_match.group(1)}",
49
- "choices": extract_approval_choices(lines[index : control_index + 1]),
50
- }
51
- for index in range(control_index, -1, -1):
52
- line = lines[index]
53
- if "Would you like to run the following command" not in line:
54
- continue
55
- return {
56
- "agent_id": agent_id,
57
- "state": "waiting_approval",
58
- "kind": "command",
59
- "command": extract_command_approval_subject(lines[: control_index + 1], index),
60
- "prompt": line.strip(),
61
- "choices": extract_approval_choices(lines[index : control_index + 1]),
62
- }
63
- return {
64
- "agent_id": agent_id,
65
- "state": "waiting_approval",
66
- "kind": "unknown",
67
- "prompt": "approval prompt detected",
68
- "choices": extract_approval_choices(lines[: control_index + 1]),
69
- }
70
-
71
-
72
- def active_approval_control_index(lines: list[str]) -> int | None:
73
- control_indices = [
74
- index
75
- for index, line in enumerate(lines)
76
- if is_approval_control_line(line)
77
- ]
78
- if not control_indices:
79
- return None
80
- control_index = control_indices[-1]
81
- if any(line.strip() for line in lines[control_index + 1 :]):
82
- return None
83
- return control_index
84
-
85
-
86
- def is_approval_control_line(line: str) -> bool:
87
- normalized = line.lower()
88
- return "enter to submit | esc to cancel" in normalized or ("esc to cancel" in normalized and "tab to amend" in normalized)
89
-
90
-
91
- def extract_approval_choices(lines: list[str]) -> list[str]:
92
- choices: list[str] = []
93
- for line in lines:
94
- stripped = line.strip()
95
- match = APPROVAL_CHOICE_RE.match(stripped)
96
- if not match:
97
- continue
98
- label = match.group(2).strip()
99
- if label and label not in choices:
100
- choices.append(label)
101
- return choices
102
-
103
-
104
- def line_is_approval_choice(line: str) -> bool:
105
- return APPROVAL_CHOICE_RE.match(line.strip()) is not None
106
-
107
-
108
- def extract_command_approval_subject(lines: list[str], prompt_index: int) -> str | None:
109
- for line in reversed(lines[:prompt_index]):
110
- stripped = line.strip()
111
- if stripped.startswith("Bash(") or stripped.startswith("Shell("):
112
- return stripped[:200]
113
- for line in lines[prompt_index + 1 : prompt_index + 8]:
114
- stripped = line.strip()
115
- if stripped.startswith("Bash(") or stripped.startswith("Shell("):
116
- return stripped[:200]
117
- return None
118
-
119
-
120
- def active_approval_choice_index(text: str) -> int | None:
121
- for line in text.splitlines():
122
- stripped = line.strip()
123
- if not (stripped.startswith("›") or stripped.startswith("❯") or stripped.startswith(">")):
124
- continue
125
- match = re.match(r"[›❯>]\s*(\d+)\.", stripped)
126
- if match:
127
- return int(match.group(1)) - 1
128
- return None
129
-
130
-
131
- def capture_has_team_orchestrator_mcp_prompt(text: str) -> bool:
132
- return (
133
- "Allow the team_orchestrator MCP server to run tool" in text
134
- or re.search(r"\bteam_orchestrator\s*[-.]\s*[A-Za-z_][A-Za-z0-9_]*\b", text) is not None
135
- )
136
-
137
-
138
- def approval_prompt_fingerprint(prompt: dict[str, Any]) -> str:
139
- data = {
140
- "kind": prompt.get("kind"),
141
- "tool": prompt.get("tool"),
142
- "prompt": prompt.get("prompt"),
143
- "choices": prompt.get("choices") or [],
144
- }
145
- return hashlib.sha256(json.dumps(data, sort_keys=True, ensure_ascii=False).encode("utf-8")).hexdigest()[:16]
146
-
147
-
148
- def choose_internal_mcp_approval_choice(prompt: dict[str, Any]) -> str:
149
- choices = prompt.get("choices") or []
150
- if INTERNAL_MCP_APPROVAL_CHOICE in choices:
151
- return INTERNAL_MCP_APPROVAL_CHOICE
152
- for choice in choices:
153
- if str(choice).startswith("Yes, and don't ask again"):
154
- return str(choice)
155
- if "Allow" in choices:
156
- return "Allow"
157
- if "Yes" in choices:
158
- return "Yes"
159
- return INTERNAL_MCP_APPROVAL_CHOICE
160
-
161
-
162
- def approval_choice_keys(prompt: dict[str, Any], capture_text: str, choice: str) -> list[str]:
163
- choices = prompt.get("choices") or []
164
- try:
165
- target_index = choices.index(choice)
166
- except ValueError:
167
- return ["Down", "Enter"]
168
- active_index = active_approval_choice_index(capture_text)
169
- if active_index is None:
170
- return [str(target_index + 1), "Enter"]
171
- delta = target_index - active_index
172
- if delta > 0:
173
- return ["Down"] * delta + ["Enter"]
174
- if delta < 0:
175
- return ["Up"] * abs(delta) + ["Enter"]
176
- return ["Enter"]