@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,774 @@
1
+ //! step 9 · transport — 控制面 trait + typed payload(ROUND-0 SKELETON).
2
+ //!
3
+ //! 设计真相源:`docs/phase0/transport-backend-design.md` §1(Rust 草图:Target /
4
+ //! InjectPayload / Key / CaptureRange / PaneField / Liveness / SpawnResult /
5
+ //! PaneInfo / Transport trait §1.3 / BackendKind / SetEnvOutcome / TransportError)。
6
+ //! InjectReport 的阶段化字段(InjectStage / InjectVerification / SubmitVerification /
7
+ //! TurnVerification)来自子系统卡 `docs/phase0/subsystems/09-transport.md`(表 §38-42)。
8
+ //!
9
+ //! Python 真相源(`team-agent-public` @ v0.2.11):`messaging/tmux_io.py`、
10
+ //! `messaging/tmux_prompt.py`、`terminal.py`、`sessions/*`(capture/resume/inventory/drift)。
11
+ //!
12
+ //! 本文件**只有类型与方法签名**,无实现(§4 铁律:RED 契约要 name 的 TYPES 先编过)。
13
+ //!
14
+ //! §10:transport 被 coordinator/lifecycle/daemon 调用,所有 daemon 可达方法返
15
+ //! `Result<_, TransportError>`(I/O / 子进程错误);**能力性拒绝**(set-env 不支持事后
16
+ //! 补种、attach 无概念)用 typed `enum` variant 表达(SetEnvOutcome / AttachOutcome),
17
+ //! **不**用 `Result::Err` / `unwrap` / `expect` / `panic`。`#![deny(...)]` 由 leader
18
+ //! 在集成时统一加,本骨架不加。
19
+
20
+ // §10:daemon 可达方法实现层禁 unwrap/expect/panic(unimplemented! 未实现 stub 不算)。
21
+ #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
22
+
23
+ use std::collections::BTreeMap;
24
+ use std::path::{Path, PathBuf};
25
+
26
+ use serde::{Deserialize, Serialize};
27
+ use thiserror::Error;
28
+
29
+ #[cfg(test)]
30
+ pub mod test_support;
31
+
32
+ // 裁决(transport-backend-design.md §1.2 / 本 lane ADJUDICATION):
33
+ // 文档草图的 `Liveness {Live/Dead/Unknown}` 就是既有的 model::enums::PaneLiveness
34
+ // (`state.py:336-341`,bug-085 穷尽三态)—— REUSE,不重定义。
35
+ pub use crate::model::enums::PaneLiveness;
36
+
37
+ // ─────────────────────────────────────────────────────────────────────────────
38
+ // id / name newtypes(透明 String 包装,字节 == 裸字符串,沿用 model::ids 风格)
39
+ // ─────────────────────────────────────────────────────────────────────────────
40
+
41
+ /// tmux `#{pane_id}`(如 `%7`)/ wezterm 整数 pane-id / conpty 内部 handle key。
42
+ /// 禁与 window/session 名混传(09-transport.md 表 §30)。
43
+ #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
44
+ #[serde(transparent)]
45
+ pub struct PaneId(pub String);
46
+
47
+ impl PaneId {
48
+ pub fn new(s: impl Into<String>) -> Self {
49
+ Self(s.into())
50
+ }
51
+ pub fn as_str(&self) -> &str {
52
+ &self.0
53
+ }
54
+ }
55
+
56
+ impl std::fmt::Display for PaneId {
57
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58
+ f.write_str(&self.0)
59
+ }
60
+ }
61
+
62
+ impl From<&str> for PaneId {
63
+ fn from(s: &str) -> Self {
64
+ Self(s.to_string())
65
+ }
66
+ }
67
+
68
+ /// tmux session 名(`tmux_session_exists`,terminal.py:20-33)。
69
+ #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
70
+ #[serde(transparent)]
71
+ pub struct SessionName(pub String);
72
+
73
+ impl SessionName {
74
+ pub fn new(s: impl Into<String>) -> Self {
75
+ Self(s.into())
76
+ }
77
+ pub fn as_str(&self) -> &str {
78
+ &self.0
79
+ }
80
+ }
81
+
82
+ impl std::fmt::Display for SessionName {
83
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84
+ f.write_str(&self.0)
85
+ }
86
+ }
87
+
88
+ impl From<&str> for SessionName {
89
+ fn from(s: &str) -> Self {
90
+ Self(s.to_string())
91
+ }
92
+ }
93
+
94
+ /// tmux window 名(`tmux_window_exists`,terminal.py:20-33)。
95
+ #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
96
+ #[serde(transparent)]
97
+ pub struct WindowName(pub String);
98
+
99
+ impl WindowName {
100
+ pub fn new(s: impl Into<String>) -> Self {
101
+ Self(s.into())
102
+ }
103
+ pub fn as_str(&self) -> &str {
104
+ &self.0
105
+ }
106
+ }
107
+
108
+ impl std::fmt::Display for WindowName {
109
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110
+ f.write_str(&self.0)
111
+ }
112
+ }
113
+
114
+ impl From<&str> for WindowName {
115
+ fn from(s: &str) -> Self {
116
+ Self(s.to_string())
117
+ }
118
+ }
119
+
120
+ // ─────────────────────────────────────────────────────────────────────────────
121
+ // 关键 typed payload(避免散字符串,§19/§22;transport-backend-design.md §1.2)
122
+ // ─────────────────────────────────────────────────────────────────────────────
123
+
124
+ /// 注入目标:两种合法寻址,类型上区分(禁混传)。tmux 用 SessionWindow 或 Pane;
125
+ /// WezTerm 只认整数 pane-id(SessionWindow 在后端内部先解析成 pane-id);ConPTY 只认
126
+ /// 自有 pane 的内部 key。
127
+ #[derive(Debug, Clone, PartialEq, Eq)]
128
+ pub enum Target {
129
+ /// tmux `%7` / wezterm 整数 pane-id / conpty 内部 handle key。
130
+ Pane(PaneId),
131
+ SessionWindow {
132
+ session: SessionName,
133
+ window: WindowName,
134
+ },
135
+ }
136
+
137
+ /// 注入载荷在类型上分流空文本(trust turn-integrity 契约 §3:空文本禁走 buffer,
138
+ /// 直发 Enter;tmux 拒空 buffer 会卡 trust prompt)。
139
+ #[derive(Debug, Clone, PartialEq, Eq)]
140
+ pub enum InjectPayload {
141
+ /// → 纯 send submit-key。
142
+ Empty,
143
+ Text(String),
144
+ }
145
+
146
+ /// 抽象 Key 枚举(§gap-5):各后端翻译,不透传 tmux 字面量。
147
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
148
+ pub enum Key {
149
+ /// tmux `Enter` / VT `\r`(CR,非 LF —— provider 提交触发的是 \r)。
150
+ Enter,
151
+ /// tmux 名 / VT `\x1b[A..D`(application-cursor-mode `\x1bO?` 风险)。
152
+ Up,
153
+ Down,
154
+ Left,
155
+ Right,
156
+ /// 选项键/数字。
157
+ Char(char),
158
+ /// `\x03`。
159
+ CtrlC,
160
+ /// tmux `-X cancel` / `q` / `d`;非 tmux 后端无 copy-mode 概念 → no-op。
161
+ CancelMode,
162
+ }
163
+
164
+ /// capture 范围(tmux `-S -<N>` / `-S -`)。
165
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
166
+ pub enum CaptureRange {
167
+ Tail(u32),
168
+ Full,
169
+ }
170
+
171
+ /// 单字段查询(display-message -p -F);非 tmux 后端无对应概念的字段返回 typed
172
+ /// 「不适用」(trait `query` 返回 `Option<String>`)。
173
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
174
+ pub enum PaneField {
175
+ PaneId,
176
+ PaneMode,
177
+ PaneWidth,
178
+ PaneCurrentCommand,
179
+ PaneCurrentPath,
180
+ SessionName,
181
+ }
182
+
183
+ /// 后端种类(诊断/事件用)。
184
+ ///
185
+ /// 裁决(本 lane ADJUDICATION):既有 `model::enums::Backend` 是 `{Tmux, Pty}`
186
+ /// (spec.py:255 的两值 spec 字段),**不等于** `BackendKind {Tmux, WezTerm, ConPty}`
187
+ /// (transport-backend-design.md §1.3 的三后端运行时种类)—— 故此处**新定义**
188
+ /// `BackendKind`,不复用 `Backend`。两者语义不同:`Backend` 是 spec 声明面的粗粒度
189
+ /// 选择(tmux vs pty),`BackendKind` 是 transport 实现面的具体后端(pty 进一步分
190
+ /// WezTerm 外部 mux 与 ConPTY 单机)。NOTE 给 leader 集成时复核映射关系
191
+ /// (Backend::Pty → {WezTerm | ConPty} 的解析在 §5 probe-first 逻辑里)。
192
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
193
+ #[serde(rename_all = "snake_case")]
194
+ pub enum BackendKind {
195
+ Tmux,
196
+ WezTerm,
197
+ ConPty,
198
+ }
199
+
200
+ /// spawn 返回:后端把它创建的终端的稳定 id 交回框架,供后续 RIE 寻址 + 身份正向
201
+ /// 登记(§4a)。
202
+ #[derive(Debug, Clone, PartialEq, Eq)]
203
+ pub struct SpawnResult {
204
+ pub pane_id: PaneId,
205
+ pub session: SessionName,
206
+ pub window: WindowName,
207
+ /// ConPTY 自有;tmux/wezterm 视 list 是否给 pid([真机])。
208
+ pub child_pid: Option<u32>,
209
+ }
210
+
211
+ /// 全局枚举的一行(身份地基)。`leader_env` 在 tmux 后端靠反向读进程 env,在
212
+ /// WezTerm/ConPTY 后端靠正向登记表投影(§4a)。
213
+ #[derive(Debug, Clone, PartialEq, Eq)]
214
+ pub struct PaneInfo {
215
+ pub pane_id: PaneId,
216
+ pub session: SessionName,
217
+ pub window_index: Option<u32>,
218
+ pub window_name: Option<WindowName>,
219
+ pub pane_index: Option<u32>,
220
+ pub tty: Option<String>,
221
+ /// WezTerm [真机]:list json 未必给前台命令 → GAP-3。
222
+ pub current_command: Option<String>,
223
+ pub current_path: Option<PathBuf>,
224
+ pub active: bool,
225
+ /// WezTerm [真机]:list json 未必给 OS pid → GAP-2。
226
+ pub pane_pid: Option<u32>,
227
+ /// tmux=反向读;wezterm/conpty=正向登记表的投影。
228
+ pub leader_env: BTreeMap<String, String>,
229
+ }
230
+
231
+ /// 抓取到的屏幕文本(recognizer 的唯一输入;backend-agnostic,§4b)。
232
+ /// transport 出口处统一规范化(rstrip / `\xa0` 归一),recognizer 不为后端开分支。
233
+ #[derive(Debug, Clone, PartialEq, Eq)]
234
+ pub struct CapturedText {
235
+ /// 规范化后的明文(各后端 capture 管线差异在此收口)。
236
+ pub text: String,
237
+ /// 抓取范围(诊断/审计)。
238
+ pub range: CaptureRange,
239
+ }
240
+
241
+ // ─────────────────────────────────────────────────────────────────────────────
242
+ // inject 流水线阶段化报告(09-transport.md InjectOutcome → InjectReport,表 §38-42)
243
+ // ─────────────────────────────────────────────────────────────────────────────
244
+
245
+ /// 注入流水线各阶段(失败定位;构成 `TransportError::Inject` 的定位字段)。
246
+ /// 字符串值取自 09-transport.md 表 §41(kebab-case,显式 `rename`)。
247
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
248
+ pub enum InjectStage {
249
+ #[serde(rename = "send-keys")]
250
+ SendKeys,
251
+ #[serde(rename = "pre-paste-capture")]
252
+ PrePasteCapture,
253
+ #[serde(rename = "set-buffer")]
254
+ SetBuffer,
255
+ #[serde(rename = "load-buffer")]
256
+ LoadBuffer,
257
+ #[serde(rename = "paste-buffer")]
258
+ PasteBuffer,
259
+ #[serde(rename = "delete-buffer")]
260
+ DeleteBuffer,
261
+ #[serde(rename = "pre-paste-pane-state")]
262
+ PrePastePaneState,
263
+ #[serde(rename = "pane-mode-check")]
264
+ PaneModeCheck,
265
+ #[serde(rename = "submit")]
266
+ Submit,
267
+ #[serde(rename = "visible-check")]
268
+ VisibleCheck,
269
+ }
270
+
271
+ /// 注入可见性验证(审计语义;tmux_io.py + tmux_prompt.py 散布字符串穷尽枚举,
272
+ /// 09-transport.md 表 §38)。
273
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
274
+ #[serde(rename_all = "snake_case")]
275
+ pub enum InjectVerification {
276
+ CaptureContainsToken,
277
+ CaptureContainsMessageFragment,
278
+ CaptureContainsNewPastedContentPrompt,
279
+ NoToken,
280
+ CaptureMissingToken,
281
+ /// 空文本走纯 send-keys(InjectPayload::Empty)的验证。
282
+ EmptyTextSendKeys,
283
+ }
284
+
285
+ /// 提交键验证(tmux_io.py:214-221, tmux_prompt.py:281-322,09-transport.md 表 §39)。
286
+ /// `{key}_sent_after_visible_token` 是模板 → 用携带 Key 的 variant 表达。
287
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
288
+ pub enum SubmitVerification {
289
+ /// `enter_sent_without_placeholder_check`。
290
+ EnterSentWithoutPlaceholderCheck,
291
+ /// `pasted_content_prompt_absent_after_submit`。
292
+ PastedContentPromptAbsentAfterSubmit,
293
+ /// `{key}_sent_after_visible_token`(key 由 variant 携带)。
294
+ KeySentAfterVisibleToken { key: Key },
295
+ /// `send_keys_failed`。
296
+ SendKeysFailed,
297
+ }
298
+
299
+ /// turn-boundary 观测(tmux_io.py:224-260,09-transport.md 表 §40)。
300
+ /// **Gap42**:turn 观测仅 metadata,`not_yet_observed` 也算成功,绝非投递闸门。
301
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
302
+ #[serde(rename_all = "snake_case")]
303
+ pub enum TurnVerification {
304
+ LeaderNewTurnBoundaryVerified,
305
+ LeaderNewTurnBoundaryMissing,
306
+ NotYetObserved,
307
+ NotRequired,
308
+ }
309
+
310
+ /// 注入流水线阶段化报告(09-transport.md InjectReport;daemon 路径 typed 返回值)。
311
+ #[derive(Debug, Clone, PartialEq, Eq)]
312
+ pub struct InjectReport {
313
+ pub stage_reached: InjectStage,
314
+ pub inject_verification: InjectVerification,
315
+ pub submit_verification: SubmitVerification,
316
+ /// Gap42:仅 metadata,`not_yet_observed` 也算成功。
317
+ pub turn_verification: TurnVerification,
318
+ pub attempts: u32,
319
+ }
320
+
321
+ // ─────────────────────────────────────────────────────────────────────────────
322
+ // 能力性拒绝的 typed 结果(非 Err —— 已知能力差,审计用;§10)
323
+ // ─────────────────────────────────────────────────────────────────────────────
324
+
325
+ /// `set_session_env` 的 typed 结果(transport-backend-design.md §1.3 / §4c)。
326
+ #[derive(Debug, Clone, PartialEq, Eq)]
327
+ pub enum SetEnvOutcome {
328
+ /// tmux:写进 session env。
329
+ Applied,
330
+ /// wezterm/conpty:worker env 已在 spawn 时注入,无需事后补种。
331
+ InternalizedAtSpawn,
332
+ /// 外部 leader pane 无法事后补种 → leader 必须启动时自带 env(§4c)。
333
+ UnsupportedForExternalPane { reason: String },
334
+ }
335
+
336
+ /// `attach_session` 的 typed 结果(transport-backend-design.md §1.3)。
337
+ #[derive(Debug, Clone, PartialEq, Eq)]
338
+ pub enum AttachOutcome {
339
+ Attached,
340
+ /// wezterm:GUI 启动即 attach,无独立动作。
341
+ GuiAttachIsImplicit,
342
+ /// conpty:无 attach 概念。
343
+ Unsupported { reason: String },
344
+ }
345
+
346
+ // ─────────────────────────────────────────────────────────────────────────────
347
+ // TransportError(thiserror,lib 边界;transport-backend-design.md §1.3)
348
+ // ─────────────────────────────────────────────────────────────────────────────
349
+
350
+ /// transport 的 I/O / 子进程错误。能力性拒绝**不**走这里(用上面的 typed outcome)。
351
+ #[derive(Debug, Error)]
352
+ pub enum TransportError {
353
+ #[error("spawn failed on {backend:?}: {source}")]
354
+ Spawn {
355
+ backend: BackendKind,
356
+ #[source]
357
+ source: std::io::Error,
358
+ },
359
+ #[error("inject failed at stage {stage:?}: {source}")]
360
+ Inject {
361
+ stage: InjectStage,
362
+ #[source]
363
+ source: std::io::Error,
364
+ },
365
+ #[error("capture failed: {source}")]
366
+ Capture {
367
+ #[source]
368
+ source: std::io::Error,
369
+ },
370
+ /// tmux/wezterm cli 非 0 退出。
371
+ #[error("subprocess {argv:?} exited with {code:?}: {stderr}")]
372
+ Subprocess {
373
+ argv: Vec<String>,
374
+ code: Option<i32>,
375
+ stderr: String,
376
+ },
377
+ /// wezterm mux 连不上 / tmux server 不在。
378
+ #[error("mux unavailable on {backend:?}: {detail}")]
379
+ MuxUnavailable { backend: BackendKind, detail: String },
380
+ #[error("target not found: {target}")]
381
+ TargetNotFound { target: String },
382
+ #[error(transparent)]
383
+ Io(#[from] std::io::Error),
384
+ }
385
+
386
+ // ─────────────────────────────────────────────────────────────────────────────
387
+ // Transport trait(控制面;transport-backend-design.md §1.3)—— 仅签名,无 body
388
+ // ─────────────────────────────────────────────────────────────────────────────
389
+
390
+ /// 控制面:把字节送进按稳定 id 寻址的终端 + 枚举/探活/生命周期。
391
+ ///
392
+ /// 全部 daemon 可达方法返 `Result<_, TransportError>`(§10:I/O/子进程错误用 `Err`,
393
+ /// 能力性拒绝用 typed outcome variant)。投递语义(发给谁/为何发/可见性验证启发式)
394
+ /// 在 transport **之上**(step 10/11),不进 trait(§gap-recognizer)。
395
+ pub trait Transport: Send + Sync {
396
+ /// 后端种类(诊断/事件用)。
397
+ fn kind(&self) -> BackendKind;
398
+
399
+ // —— SPAWN(ST):所有后端天然满足;cwd/env 是 spawn 参数,无独立动词(§gap-setenv)——
400
+
401
+ /// tmux=`new-session -d` / wezterm=`spawn --new-window` / conpty=`openpty`+spawn。
402
+ fn spawn_first(
403
+ &self,
404
+ session: &SessionName,
405
+ window: &WindowName,
406
+ argv: &[String],
407
+ cwd: &Path,
408
+ env: &BTreeMap<String, String>,
409
+ ) -> Result<SpawnResult, TransportError>;
410
+
411
+ /// tmux=`new-window` / wezterm=`spawn --pane-id 锚` / conpty=再 `openpty` 进内存表。
412
+ fn spawn_into(
413
+ &self,
414
+ session: &SessionName,
415
+ window: &WindowName,
416
+ argv: &[String],
417
+ cwd: &Path,
418
+ env: &BTreeMap<String, String>,
419
+ ) -> Result<SpawnResult, TransportError>;
420
+
421
+ // —— INJECT / CAPTURE / QUERY(RIE):按稳定 Target 寻址 ——
422
+
423
+ /// 归并 set/load-buffer + paste-buffer + send submit;空文本走纯 send-keys
424
+ /// (`InjectPayload::Empty`)。仅保证「字节进去了 + 提交键发了」+ typed 阶段化报告;
425
+ /// 可见性验证启发式在 transport 之上(§gap-recognizer)。
426
+ fn inject(
427
+ &self,
428
+ target: &Target,
429
+ payload: &InjectPayload,
430
+ submit: Key,
431
+ bracketed: bool,
432
+ ) -> Result<InjectReport, TransportError>;
433
+
434
+ fn send_keys(&self, target: &Target, keys: &[Key]) -> Result<(), TransportError>;
435
+
436
+ fn capture(
437
+ &self,
438
+ target: &Target,
439
+ range: CaptureRange,
440
+ ) -> Result<CapturedText, TransportError>;
441
+
442
+ /// 非 tmux 后端无对应概念的字段返回 `Ok(None)`(typed「不适用」)。
443
+ fn query(&self, target: &Target, field: PaneField)
444
+ -> Result<Option<String>, TransportError>;
445
+
446
+ /// pane 存活三态(`PaneLiveness`,bug-085 穷尽 match;unknown ≠ dead ≠ live)。
447
+ fn liveness(&self, pane: &PaneId) -> Result<PaneLiveness, TransportError>;
448
+
449
+ // —— ENUMERATE / IDENTITY(SL + 进程探测):身份/rebind 地基 ——
450
+
451
+ /// 全局枚举所有 pane + 每 pane 的 leader_env。tmux=`list-panes -a` + 读进程 env;
452
+ /// wezterm=`cli list --format json` + 正向登记表;conpty=daemon 内存表(仅自有,§4a)。
453
+ fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError>;
454
+
455
+ fn has_session(&self, session: &SessionName) -> Result<bool, TransportError>;
456
+
457
+ fn list_windows(
458
+ &self,
459
+ session: &SessionName,
460
+ ) -> Result<Vec<WindowName>, TransportError>;
461
+
462
+ /// tmux=`set-environment`;无 server-env 的后端(WezTerm/ConPTY)对 worker 内化为
463
+ /// 「spawn 时注入」(`InternalizedAtSpawn`),对外部 leader pane 返回 typed 不支持
464
+ /// (`UnsupportedForExternalPane`,§4c)。
465
+ fn set_session_env(
466
+ &self,
467
+ session: &SessionName,
468
+ key: &str,
469
+ value: &str,
470
+ ) -> Result<SetEnvOutcome, TransportError>;
471
+
472
+ // —— LIFECYCLE(SL)——
473
+
474
+ fn kill_session(&self, session: &SessionName) -> Result<(), TransportError>;
475
+
476
+ fn kill_window(&self, target: &Target) -> Result<(), TransportError>;
477
+
478
+ /// 交互前台 attach(leader 用)。tmux=`attach-session`;wezterm=GUI 即 attach
479
+ /// (`GuiAttachIsImplicit`);conpty=typed 不支持(`Unsupported`)。
480
+ fn attach_session(
481
+ &self,
482
+ session: &SessionName,
483
+ ) -> Result<AttachOutcome, TransportError>;
484
+ }
485
+
486
+ // ─────────────────────────────────────────────────────────────────────────────
487
+ // 命令构造 seam(STEP-9 头号目标:exact tmux command construction).
488
+ //
489
+ // trait 只回高层 typed 值(SpawnResult/InjectReport/CapturedText/Option<String>),
490
+ // 永远拿不到它构的 argv —— 因此「set/load/paste-buffer -p、capture-pane -S -<N>、
491
+ // display-message -F #{pane_width}、send-keys 键名翻译、CancelMode→-X cancel/q/d」
492
+ // 这些 golden 在 trait 层**无法断言**。这里引入纯函数构造 seam(无 I/O,可单测):
493
+ // tmux 后端的实现必须用这些 fn 构 argv,契约则 golden-lock 它们的输出。
494
+ // 真相源:tmux_io.py / tmux_prompt.py / runtime.py / delivery.py / terminal.py /
495
+ // provider_cli/codex.py(argv 已 golden-probe via /tmp/transport_cmd_golden.py)。
496
+ // ─────────────────────────────────────────────────────────────────────────────
497
+
498
+ /// tmux 进入的特殊 mode(CancelMode 退出键分派的输入;tmux_io.py:416-427)。
499
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
500
+ pub enum PaneMode {
501
+ /// `copy-mode` → `-X cancel`。
502
+ Copy,
503
+ /// `tree-mode` → `q`。
504
+ Tree,
505
+ /// `view-mode` → `q`。
506
+ View,
507
+ /// `client-mode` → `d`。
508
+ Client,
509
+ /// 其它/未知 → `-X cancel` + warn(`pane_mode_unknown_cancel_attempted`)。
510
+ Unknown,
511
+ }
512
+
513
+ /// 把抽象 `Key` 翻译成 tmux send-keys 字面键名(各后端翻译,不透传字面量;§gap-5)。
514
+ /// tmux:Enter/Up/Down/Left/Right/数字字符/C-c;CancelMode 不是单键(走 cancel_mode_argv)。
515
+ /// 真相源:codex.py:266 `send-keys -t %7 Down Enter`、tmux send-keys 键名约定。
516
+ pub fn tmux_key_name(key: Key) -> &'static str {
517
+ match key {
518
+ Key::Enter => "Enter",
519
+ Key::Up => "Up",
520
+ Key::Down => "Down",
521
+ Key::Left => "Left",
522
+ Key::Right => "Right",
523
+ Key::Char('0') => "0",
524
+ Key::Char('1') => "1",
525
+ Key::Char('2') => "2",
526
+ Key::Char('3') => "3",
527
+ Key::Char('4') => "4",
528
+ Key::Char('5') => "5",
529
+ Key::Char('6') => "6",
530
+ Key::Char('7') => "7",
531
+ Key::Char('8') => "8",
532
+ Key::Char('9') => "9",
533
+ Key::CtrlC => "C-c",
534
+ Key::CancelMode | Key::Char(_) => "",
535
+ }
536
+ }
537
+
538
+ /// `send-keys -t <target> <k1> <k2> ...`(键名经 `tmux_key_name` 翻译)。
539
+ /// golden:`[Down, Enter]` → `["tmux","send-keys","-t","%7","Down","Enter"]`(codex.py:266)。
540
+ pub fn tmux_send_keys_argv(pane: &PaneId, keys: &[Key]) -> Vec<String> {
541
+ let mut argv = vec![
542
+ "tmux".to_string(),
543
+ "send-keys".to_string(),
544
+ "-t".to_string(),
545
+ pane.as_str().to_string(),
546
+ ];
547
+ argv.extend(
548
+ keys.iter()
549
+ .copied()
550
+ .map(tmux_key_name)
551
+ .filter(|k| !k.is_empty())
552
+ .map(str::to_string),
553
+ );
554
+ argv
555
+ }
556
+
557
+ /// CancelMode 在 tmux 上按 pane mode 分派退出键(tmux_io.py:419-426)。
558
+ /// golden:Copy→`-X cancel`,Tree/View→`q`,Client→`d`,Unknown→`-X cancel`(+warn)。
559
+ pub fn tmux_cancel_mode_argv(pane: &PaneId, mode: PaneMode) -> Vec<String> {
560
+ let mut argv = vec![
561
+ "tmux".to_string(),
562
+ "send-keys".to_string(),
563
+ "-t".to_string(),
564
+ pane.as_str().to_string(),
565
+ ];
566
+ match mode {
567
+ PaneMode::Copy | PaneMode::Unknown => {
568
+ argv.push("-X".to_string());
569
+ argv.push("cancel".to_string());
570
+ }
571
+ PaneMode::Tree | PaneMode::View => argv.push("q".to_string()),
572
+ PaneMode::Client => argv.push("d".to_string()),
573
+ }
574
+ argv
575
+ }
576
+
577
+ /// CaptureRange → `capture-pane -p -S <spec> -t <target>`。
578
+ /// golden:`Tail(40)` → `-S -40`(tmux_prompt.py:149/tmux_io.py:410);
579
+ /// `Full` → `-S -`(runtime.py:519)。
580
+ pub fn tmux_capture_argv(pane: &PaneId, range: CaptureRange) -> Vec<String> {
581
+ let spec = match range {
582
+ CaptureRange::Tail(lines) => format!("-{lines}"),
583
+ CaptureRange::Full => "-".to_string(),
584
+ };
585
+ vec![
586
+ "tmux".to_string(),
587
+ "capture-pane".to_string(),
588
+ "-p".to_string(),
589
+ "-S".to_string(),
590
+ spec,
591
+ "-t".to_string(),
592
+ pane.as_str().to_string(),
593
+ ]
594
+ }
595
+
596
+ /// PaneField → `display-message -p -t <target> [-F] <fmt>`。
597
+ /// golden:PaneWidth → `-F '#{pane_width}'`(delivery.py:34);
598
+ /// PaneMode → `'#{pane_mode}'`(tmux_io.py:403);PaneId → `'#{pane_id}'`(state.py:346)。
599
+ pub fn tmux_query_argv(pane: &PaneId, field: PaneField) -> Vec<String> {
600
+ let fmt = match field {
601
+ PaneField::PaneId => "#{pane_id}",
602
+ PaneField::PaneMode => "#{pane_mode}",
603
+ PaneField::PaneWidth => "#{pane_width}",
604
+ PaneField::PaneCurrentCommand => "#{pane_current_command}",
605
+ PaneField::PaneCurrentPath => "#{pane_current_path}",
606
+ PaneField::SessionName => "#{session_name}",
607
+ };
608
+ let mut argv = vec![
609
+ "tmux".to_string(),
610
+ "display-message".to_string(),
611
+ "-p".to_string(),
612
+ "-t".to_string(),
613
+ pane.as_str().to_string(),
614
+ ];
615
+ if !matches!(field, PaneField::PaneMode) {
616
+ argv.push("-F".to_string());
617
+ }
618
+ argv.push(fmt.to_string());
619
+ argv
620
+ }
621
+
622
+ /// spawn argv:首个 session 用 new-session,后续 worker 用 new-window。
623
+ /// golden(terminal.py:44-45 / runtime.py:1019-1020):
624
+ /// first → `new-session -d -s <s> -n <w> sh -lc <cmd>`
625
+ /// into → `new-window -t <s> -n <w> sh -lc <cmd>`
626
+ /// `argv` 被组装成单条 `sh -lc` 命令字符串(provider 启动行)。
627
+ pub fn tmux_spawn_argv(
628
+ session: &SessionName,
629
+ window: &WindowName,
630
+ command: &str,
631
+ first: bool,
632
+ ) -> Vec<String> {
633
+ if first {
634
+ vec![
635
+ "tmux".to_string(),
636
+ "new-session".to_string(),
637
+ "-d".to_string(),
638
+ "-s".to_string(),
639
+ session.as_str().to_string(),
640
+ "-n".to_string(),
641
+ window.as_str().to_string(),
642
+ "sh".to_string(),
643
+ "-lc".to_string(),
644
+ command.to_string(),
645
+ ]
646
+ } else {
647
+ vec![
648
+ "tmux".to_string(),
649
+ "new-window".to_string(),
650
+ "-t".to_string(),
651
+ session.as_str().to_string(),
652
+ "-n".to_string(),
653
+ window.as_str().to_string(),
654
+ "sh".to_string(),
655
+ "-lc".to_string(),
656
+ command.to_string(),
657
+ ]
658
+ }
659
+ }
660
+
661
+ /// inject 文本路径的 buffer 构造序列(空文本不走这里,走 tmux_empty_inject_argv)。
662
+ /// 返回有序 argv 列表:set-buffer(小)或 load-buffer -(大,>=阈值)→ paste-buffer -p →
663
+ /// delete-buffer。golden(tmux_io.py:119/303/314/324):
664
+ /// set-buffer → `set-buffer -b <buf> <text>`
665
+ /// paste-buffer→ `paste-buffer -t <target> -b <buf> -p`(-p = bracketed)
666
+ /// delete-buffer→`delete-buffer -b <buf>`
667
+ pub fn tmux_inject_text_argv(
668
+ pane: &PaneId,
669
+ buffer_name: &str,
670
+ text: &str,
671
+ bracketed: bool,
672
+ ) -> Vec<Vec<String>> {
673
+ const TMUX_STDIN_BUFFER_THRESHOLD: usize = 16 * 1024;
674
+ let mut paste = vec![
675
+ "tmux".to_string(),
676
+ "paste-buffer".to_string(),
677
+ "-t".to_string(),
678
+ pane.as_str().to_string(),
679
+ "-b".to_string(),
680
+ buffer_name.to_string(),
681
+ ];
682
+ if bracketed {
683
+ paste.push("-p".to_string());
684
+ }
685
+ let load = if text.len() >= TMUX_STDIN_BUFFER_THRESHOLD {
686
+ vec![
687
+ "tmux".to_string(),
688
+ "load-buffer".to_string(),
689
+ "-b".to_string(),
690
+ buffer_name.to_string(),
691
+ "-".to_string(),
692
+ ]
693
+ } else {
694
+ vec![
695
+ "tmux".to_string(),
696
+ "set-buffer".to_string(),
697
+ "-b".to_string(),
698
+ buffer_name.to_string(),
699
+ text.to_string(),
700
+ ]
701
+ };
702
+ vec![
703
+ load,
704
+ paste,
705
+ vec![
706
+ "tmux".to_string(),
707
+ "delete-buffer".to_string(),
708
+ "-b".to_string(),
709
+ buffer_name.to_string(),
710
+ ],
711
+ ]
712
+ }
713
+
714
+ /// 空文本 inject:纯 `send-keys -t <target> <submit_key>`,**禁** buffer 路径
715
+ /// (tmux 拒空 buffer 会卡 trust prompt;tmux_io.py:42)。
716
+ pub fn tmux_empty_inject_argv(pane: &PaneId, submit: Key) -> Vec<String> {
717
+ tmux_send_keys_argv(pane, &[submit])
718
+ }
719
+
720
+ /// capture 出口规范化(§4b,design line 399-400):逐行 rstrip 行尾空白 +
721
+ /// `\xa0`(NBSP)→`\x20`,保留 box-drawing。recognizer 不为后端开分支。
722
+ /// golden:`"line one \nbusy\xa0marker \n \n"` → `"line one\nbusy marker\n\n"`。
723
+ pub fn normalize_capture(raw: &str) -> String {
724
+ raw.replace('\u{a0}', " ")
725
+ .split_inclusive('\n')
726
+ .map(|line| {
727
+ if let Some(stripped) = line.strip_suffix('\n') {
728
+ let mut s = stripped.trim_end().to_string();
729
+ s.push('\n');
730
+ s
731
+ } else {
732
+ line.trim_end().to_string()
733
+ }
734
+ })
735
+ .collect()
736
+ }
737
+
738
+ /// `SubmitVerification` → wire 字符串(tmux_io.py:64/215-221, tmux_prompt.py:304/313)。
739
+ /// 模板:`{key}_sent_after_visible_token`(key 由 variant 携带,经 tmux_key_name 取名)。
740
+ /// 真相源:`enter_sent_without_placeholder_check` /
741
+ /// `pasted_content_prompt_absent_after_submit` / `send_keys_failed` /
742
+ /// `Enter_sent_after_visible_token`(submit_key 字面)。
743
+ pub fn submit_verification_wire(v: SubmitVerification) -> String {
744
+ match v {
745
+ SubmitVerification::EnterSentWithoutPlaceholderCheck => {
746
+ "enter_sent_without_placeholder_check".to_string()
747
+ }
748
+ SubmitVerification::PastedContentPromptAbsentAfterSubmit => {
749
+ "pasted_content_prompt_absent_after_submit".to_string()
750
+ }
751
+ SubmitVerification::KeySentAfterVisibleToken { key } => {
752
+ format!("{}_sent_after_visible_token", tmux_key_name(key))
753
+ }
754
+ SubmitVerification::SendKeysFailed => "send_keys_failed".to_string(),
755
+ }
756
+ }
757
+
758
+ // ─────────────────────────────────────────────────────────────────────────────
759
+ // RED contracts — step 9 transport(ROUND-0).
760
+ //
761
+ // 真相源:Python team-agent-public @ v0.2.11(`terminal.py` / `messaging/tmux_io.py`
762
+ // / `runtime.py`)golden-locked via /tmp/transport_golden.py;rust-native parity
763
+ // 来自 `docs/phase0/transport-backend-design.md` §1/§3 + `contracts-rust-native.yaml`。
764
+ //
765
+ // 这些测试通过一个 **每个方法都 `unimplemented!()`** 的 `StubBackend` 驱动 `Transport`
766
+ // trait(skeleton 无任何 impl),因此跑起来即 panic == 真 RED;porter 填实现后转 GREEN。
767
+ // 另有一批 **纯 typed-payload / serde 字节锁** 断言(命令面字符串、verification 阶梯、
768
+ // 寻址稳定性)—— 它们锁住契约形状,porter 改坏类型/serde rename 立即失败。
769
+ //
770
+ // [真机] 标记的契约(WezTerm `cli list` / B1 等)`#[ignore]` —— 无法在此完整断言,
771
+ // 仅锁形状,实现期真机校验。
772
+ // ─────────────────────────────────────────────────────────────────────────────
773
+ #[cfg(test)]
774
+ mod tests;