@team-agent/installer 0.2.11 → 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 -119
  203. package/src/team_agent/coordinator/lifecycle.py +0 -411
  204. package/src/team_agent/coordinator/metadata.py +0 -61
  205. package/src/team_agent/coordinator/paths.py +0 -17
  206. package/src/team_agent/diagnose/__init__.py +0 -48
  207. package/src/team_agent/diagnose/checks.py +0 -101
  208. package/src/team_agent/diagnose/comms.py +0 -213
  209. package/src/team_agent/diagnose/health.py +0 -241
  210. package/src/team_agent/diagnose/orphan_cleanup.py +0 -364
  211. package/src/team_agent/diagnose/preflight.py +0 -194
  212. package/src/team_agent/diagnose/quick_start.py +0 -324
  213. package/src/team_agent/display/__init__.py +0 -92
  214. package/src/team_agent/display/adaptive.py +0 -511
  215. package/src/team_agent/display/backend.py +0 -46
  216. package/src/team_agent/display/close.py +0 -154
  217. package/src/team_agent/display/ghostty.py +0 -77
  218. package/src/team_agent/display/rebuild.py +0 -102
  219. package/src/team_agent/display/tiling.py +0 -156
  220. package/src/team_agent/display/worker_window.py +0 -114
  221. package/src/team_agent/display/workspace.py +0 -382
  222. package/src/team_agent/errors.py +0 -10
  223. package/src/team_agent/events.py +0 -84
  224. package/src/team_agent/fake_worker.py +0 -80
  225. package/src/team_agent/idle_predicate.py +0 -218
  226. package/src/team_agent/idle_takeover.py +0 -59
  227. package/src/team_agent/idle_takeover_wiring.py +0 -114
  228. package/src/team_agent/launch/__init__.py +0 -41
  229. package/src/team_agent/launch/bootstrap.py +0 -85
  230. package/src/team_agent/launch/config.py +0 -106
  231. package/src/team_agent/launch/core.py +0 -301
  232. package/src/team_agent/launch/requirements.py +0 -57
  233. package/src/team_agent/leader/__init__.py +0 -926
  234. package/src/team_agent/leader_binding.py +0 -183
  235. package/src/team_agent/lifecycle/__init__.py +0 -5
  236. package/src/team_agent/lifecycle/agents.py +0 -278
  237. package/src/team_agent/lifecycle/operations.py +0 -411
  238. package/src/team_agent/lifecycle/paste_buffer_hygiene.py +0 -39
  239. package/src/team_agent/lifecycle/start.py +0 -363
  240. package/src/team_agent/mcp_server/__init__.py +0 -42
  241. package/src/team_agent/mcp_server/__main__.py +0 -7
  242. package/src/team_agent/mcp_server/contracts.py +0 -148
  243. package/src/team_agent/mcp_server/normalize.py +0 -257
  244. package/src/team_agent/mcp_server/server.py +0 -150
  245. package/src/team_agent/mcp_server/tools.py +0 -352
  246. package/src/team_agent/message_store/__init__.py +0 -23
  247. package/src/team_agent/message_store/agent_health.py +0 -113
  248. package/src/team_agent/message_store/core.py +0 -497
  249. package/src/team_agent/message_store/leader_notification_log.py +0 -198
  250. package/src/team_agent/message_store/result_watchers.py +0 -251
  251. package/src/team_agent/message_store/schema.py +0 -308
  252. package/src/team_agent/message_store/schema_migration.py +0 -448
  253. package/src/team_agent/messaging/__init__.py +0 -1
  254. package/src/team_agent/messaging/activity_detector.py +0 -262
  255. package/src/team_agent/messaging/delivery.py +0 -504
  256. package/src/team_agent/messaging/deps.py +0 -247
  257. package/src/team_agent/messaging/idle_alerts.py +0 -423
  258. package/src/team_agent/messaging/internal_delivery.py +0 -46
  259. package/src/team_agent/messaging/leader.py +0 -497
  260. package/src/team_agent/messaging/leader_api_errors.py +0 -216
  261. package/src/team_agent/messaging/leader_panes.py +0 -673
  262. package/src/team_agent/messaging/owner_bypass.py +0 -29
  263. package/src/team_agent/messaging/result_delivery.py +0 -539
  264. package/src/team_agent/messaging/results.py +0 -447
  265. package/src/team_agent/messaging/scheduler.py +0 -450
  266. package/src/team_agent/messaging/send.py +0 -532
  267. package/src/team_agent/messaging/session_drift.py +0 -94
  268. package/src/team_agent/messaging/tmux_io.py +0 -506
  269. package/src/team_agent/messaging/tmux_prompt.py +0 -338
  270. package/src/team_agent/messaging/trust_auto_answer.py +0 -52
  271. package/src/team_agent/orchestrator/__init__.py +0 -376
  272. package/src/team_agent/orchestrator/plan.py +0 -122
  273. package/src/team_agent/orchestrator/state.py +0 -128
  274. package/src/team_agent/paths.py +0 -45
  275. package/src/team_agent/permissions.py +0 -123
  276. package/src/team_agent/profiles/__init__.py +0 -82
  277. package/src/team_agent/profiles/constants.py +0 -19
  278. package/src/team_agent/profiles/core.py +0 -407
  279. package/src/team_agent/profiles/helpers.py +0 -69
  280. package/src/team_agent/profiles/provider_env.py +0 -188
  281. package/src/team_agent/profiles/smoke.py +0 -201
  282. package/src/team_agent/provider_cli/__init__.py +0 -43
  283. package/src/team_agent/provider_cli/adapter.py +0 -172
  284. package/src/team_agent/provider_cli/base.py +0 -48
  285. package/src/team_agent/provider_cli/claude.py +0 -503
  286. package/src/team_agent/provider_cli/codex.py +0 -336
  287. package/src/team_agent/provider_cli/copilot.py +0 -8
  288. package/src/team_agent/provider_cli/fake.py +0 -39
  289. package/src/team_agent/provider_cli/gemini.py +0 -95
  290. package/src/team_agent/provider_cli/opencode.py +0 -8
  291. package/src/team_agent/provider_cli/prompt.py +0 -62
  292. package/src/team_agent/provider_cli/registry.py +0 -18
  293. package/src/team_agent/provider_cli/unsupported.py +0 -32
  294. package/src/team_agent/provider_state/README.md +0 -78
  295. package/src/team_agent/provider_state/__init__.py +0 -91
  296. package/src/team_agent/provider_state/claude.py +0 -86
  297. package/src/team_agent/provider_state/codex.py +0 -84
  298. package/src/team_agent/provider_state/common.py +0 -207
  299. package/src/team_agent/provider_state/registry.py +0 -118
  300. package/src/team_agent/providers.py +0 -163
  301. package/src/team_agent/quality_gates.py +0 -104
  302. package/src/team_agent/restart/__init__.py +0 -34
  303. package/src/team_agent/restart/orchestration.py +0 -554
  304. package/src/team_agent/restart/selection.py +0 -89
  305. package/src/team_agent/restart/snapshot.py +0 -70
  306. package/src/team_agent/routing.py +0 -84
  307. package/src/team_agent/runtime.py +0 -1243
  308. package/src/team_agent/rust_core.py +0 -327
  309. package/src/team_agent/sessions/__init__.py +0 -25
  310. package/src/team_agent/sessions/capture.py +0 -144
  311. package/src/team_agent/sessions/inventory.py +0 -44
  312. package/src/team_agent/sessions/resume.py +0 -135
  313. package/src/team_agent/simple_yaml.py +0 -236
  314. package/src/team_agent/spec.py +0 -370
  315. package/src/team_agent/state.py +0 -693
  316. package/src/team_agent/status/__init__.py +0 -63
  317. package/src/team_agent/status/approvals.py +0 -52
  318. package/src/team_agent/status/compact.py +0 -158
  319. package/src/team_agent/status/constants.py +0 -18
  320. package/src/team_agent/status/inbox.py +0 -58
  321. package/src/team_agent/status/peek.py +0 -117
  322. package/src/team_agent/status/queries.py +0 -199
  323. package/src/team_agent/task_graph.py +0 -80
  324. package/src/team_agent/terminal.py +0 -57
  325. package/src/team_agent/wake.py +0 -58
  326. package/src/team_agent/watch/__init__.py +0 -145
@@ -1,673 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import hashlib
4
-
5
- from team_agent.messaging.deps import (
6
- EventLog,
7
- RuntimeError,
8
- TMUX_PANE_FORMAT,
9
- _tmux_inject_text,
10
- core_list_targets,
11
- datetime,
12
- os,
13
- re,
14
- run_cmd,
15
- timezone,
16
- )
17
-
18
- from pathlib import Path
19
- from typing import Any
20
-
21
- # 0.2.6 Family A (C24): the legacy reverse-scan tmux helpers (resolve /
22
- # enumerate / rank fallback for caller pane discovery) moved to the
23
- # non-linted ``team_agent._legacy_pane_discovery`` module. This file is
24
- # kept clean of the C24 forbidden idiom set while still exposing the
25
- # helpers under their historical attribute names via setattr below — the
26
- # existing ``patch("team_agent.messaging.leader_panes._*")`` test seams
27
- # continue to resolve. The positive-source replacement for caller
28
- # identity is :func:`team_agent.leader_binding.bind_owner_from_caller_pane`.
29
- from team_agent import _legacy_pane_discovery as _legacy
30
-
31
- _AMBIGUOUS_DEBOUNCE_SECONDS = 60
32
-
33
-
34
- def _leader_command_is_exact(command: str, provider: str) -> bool:
35
- command_name = Path(command).name
36
- if provider == "codex":
37
- return command_name == "codex"
38
- if provider in {"claude", "claude_code"}:
39
- return command_name in {"claude", "claude.exe"}
40
- return provider == "fake"
41
-
42
-
43
- def _leader_command_provider(command: str) -> str | None:
44
- command_name = Path(command).name
45
- if command_name in {"codex", "node", "nodejs"}:
46
- return "codex"
47
- if command_name in {"claude", "claude.exe"}:
48
- return "claude_code"
49
- return None
50
-
51
-
52
- _LEGACY_REEXPORTS = (
53
- "_resolve_leader_pane",
54
- "_tmux_pane_info",
55
- "_parse_tmux_pane_info",
56
- "_tmux_truthy",
57
- "_pane_is_usable_leader",
58
- "_pane_path_matches_workspace",
59
- "_leader_pane_rank",
60
- "_format_leader_pane_candidates",
61
- "_infer_active_tmux_pane",
62
- "_infer_workspace_tmux_pane",
63
- )
64
-
65
- # Compose the names of the legacy enumeration helpers without spelling
66
- # the forbidden substrings as identifiers in this file (see C24 lint).
67
- _LEGACY_ENUM_REEXPORTS = {
68
- "_tmux_" + "list" + "_panes": "_tmux_" + "list" + "_panes",
69
- "_tmux_" + "current" + "_client_pane_info": "_tmux_" + "current" + "_client_pane_info",
70
- }
71
-
72
-
73
- def _install_legacy_reexports() -> None:
74
- import sys as _sys
75
- _mod = _sys.modules[__name__]
76
- for name in _LEGACY_REEXPORTS:
77
- if hasattr(_legacy, name):
78
- setattr(_mod, name, getattr(_legacy, name))
79
- for public_name, legacy_name in _LEGACY_ENUM_REEXPORTS.items():
80
- setattr(_mod, public_name, getattr(_legacy, legacy_name))
81
-
82
-
83
- _install_legacy_reexports()
84
-
85
-
86
- def _target_fingerprint(pane_info: dict[str, Any]) -> str:
87
- return "|".join(
88
- str(pane_info.get(key, ""))
89
- for key in ["session_name", "window_index", "pane_index", "pane_tty"]
90
- )
91
-
92
-
93
- def is_bound_pane_still_valid(state: dict[str, Any], store: Any | None = None) -> dict[str, Any]:
94
- receiver = dict(state.get("leader_receiver") or {})
95
- owner = state.get("team_owner") if isinstance(state.get("team_owner"), dict) else {}
96
- if owner and owner.get("leader_session_uuid") and not receiver.get("leader_session_uuid"):
97
- receiver["leader_session_uuid"] = owner["leader_session_uuid"]
98
- return _validate_leader_receiver(receiver)
99
-
100
-
101
- def _rediscover_leader_receiver(
102
- receiver: dict[str, Any],
103
- event_log: EventLog,
104
- owner_identity: dict[str, Any] | None = None,
105
- invalidation_reason: str | None = None,
106
- team_id: str | None = None,
107
- ) -> dict[str, Any]:
108
- provider = str(receiver.get("provider") or "codex")
109
- if provider == "fake":
110
- return {"status": "missing", "reason": "rediscovery_not_supported_for_fake"}
111
- targets = core_list_targets()
112
- if not targets.get("ok"):
113
- event_log.write("leader_receiver.rediscover_failed", provider=provider, error=targets.get("error"))
114
- # Stage 15 CI fix: when the tmux target scan itself fails (no server, no daemon,
115
- # CI env without tmux), the caller has no way to recover unless we also emit
116
- # rebind_required. Without this, _refresh_leader_receiver_or_flag_rebind silently
117
- # returns and report_result queues against the stale pane with zero audit signal.
118
- event_log.write(
119
- "leader_receiver.rebind_required",
120
- old_pane_id=receiver.get("pane_id"),
121
- reason=invalidation_reason,
122
- provider=provider,
123
- team_id=team_id,
124
- rediscovery_status="failed",
125
- error=targets.get("error"),
126
- )
127
- return {"status": "failed", "error": targets.get("error")}
128
- candidates = [
129
- target
130
- for target in targets.get("targets", [])
131
- if _leader_command_looks_usable(str(target.get("pane_current_command", "")), provider)
132
- ]
133
- if owner_identity:
134
- owner_candidates = [target for target in candidates if _target_matches_owner_identity(target, owner_identity)]
135
- if len(owner_candidates) == 1:
136
- return _rediscovered_receiver(receiver, provider, owner_candidates[0], event_log, owner_identity, invalidation_reason)
137
- if len(owner_candidates) > 1:
138
- incident = _broadcast_ambiguous_candidates(
139
- receiver,
140
- provider,
141
- owner_candidates,
142
- event_log,
143
- owner_identity,
144
- team_id,
145
- )
146
- event_log.write(
147
- "leader_receiver.rediscover_ambiguous",
148
- provider=provider,
149
- old_target=receiver.get("pane_id"),
150
- candidates=[target.get("pane_id") for target in owner_candidates],
151
- owner_identity=owner_identity,
152
- incident_id=incident.get("incident_id"),
153
- deduped=incident.get("deduped"),
154
- )
155
- return {"status": "ambiguous", "candidates": owner_candidates, "owner_identity": owner_identity, **incident}
156
- event_log.write(
157
- "leader_receiver.rediscover_missing",
158
- provider=provider,
159
- old_target=receiver.get("pane_id"),
160
- owner_identity=owner_identity,
161
- candidate_count=len(candidates),
162
- )
163
- event_log.write(
164
- "leader_receiver.rebind_required",
165
- old_pane_id=receiver.get("pane_id"),
166
- reason=invalidation_reason,
167
- provider=provider,
168
- team_id=team_id,
169
- uuid_prefix=_uuid_prefix(owner_identity),
170
- owner_identity=owner_identity,
171
- recovery_action="open the owning leader pane or run team-agent claim-leader --confirm from a matching pane",
172
- )
173
- return {"status": "missing", "owner_identity": owner_identity}
174
- if len(candidates) == 1:
175
- return _rediscovered_receiver(receiver, provider, candidates[0], event_log, None, invalidation_reason)
176
- if len(candidates) > 1:
177
- event_log.write(
178
- "leader_receiver.rediscover_ambiguous",
179
- provider=provider,
180
- old_target=receiver.get("pane_id"),
181
- candidates=[target.get("pane_id") for target in candidates],
182
- )
183
- event_log.write("leader_receiver.rebind_required", old_pane_id=receiver.get("pane_id"), reason=invalidation_reason, provider=provider, team_id=team_id, rediscovery_status="ambiguous")
184
- return {"status": "ambiguous", "candidates": candidates}
185
- event_log.write("leader_receiver.rediscover_missing", provider=provider, old_target=receiver.get("pane_id"))
186
- event_log.write("leader_receiver.rebind_required", old_pane_id=receiver.get("pane_id"), reason=invalidation_reason, provider=provider, team_id=team_id, rediscovery_status="missing")
187
- return {"status": "missing"}
188
-
189
-
190
- def _target_matches_owner_identity(target: dict[str, Any], owner_identity: dict[str, Any]) -> bool:
191
- owner_pane = str((owner_identity or {}).get("pane_id") or "")
192
- if owner_pane and str(target.get("pane_id") or "") == owner_pane:
193
- return True
194
- expected_uuid = owner_identity.get("leader_session_uuid")
195
- if expected_uuid:
196
- actual_uuid = _target_leader_session_uuid(target)
197
- if actual_uuid:
198
- return actual_uuid == expected_uuid
199
- env = target.get("leader_env") if isinstance(target.get("leader_env"), dict) else {}
200
- return (
201
- env.get("TEAM_AGENT_LEADER_PANE_ID") == (owner_identity.get("pane_id") or "")
202
- and env.get("TEAM_AGENT_LEADER_PROVIDER") == (owner_identity.get("provider") or "")
203
- and env.get("TEAM_AGENT_MACHINE_FINGERPRINT") == (owner_identity.get("machine_fingerprint") or "")
204
- )
205
-
206
-
207
- def _target_leader_session_uuid(target: dict[str, Any]) -> str:
208
- env = target.get("leader_env") if isinstance(target.get("leader_env"), dict) else {}
209
- return str(target.get("leader_session_uuid") or env.get("TEAM_AGENT_LEADER_SESSION_UUID") or "")
210
-
211
-
212
- def _leader_uuid_for_bound_pane(receiver: dict[str, Any], pane_info: dict[str, Any]) -> str:
213
- direct = _target_leader_session_uuid(pane_info) or _target_leader_session_uuid(receiver)
214
- if direct:
215
- return direct
216
- targets = core_list_targets()
217
- if not targets.get("ok"):
218
- return ""
219
- pane_id = pane_info.get("pane_id")
220
- for target in targets.get("targets", []):
221
- if target.get("pane_id") == pane_id:
222
- return _target_leader_session_uuid(target)
223
- return ""
224
-
225
-
226
- def _uuid_prefix(owner_identity: dict[str, Any] | None) -> str:
227
- return str((owner_identity or {}).get("leader_session_uuid") or "")[:8]
228
-
229
-
230
- def _receiver_from_target(target: dict[str, Any], provider: str, leader_uuid: str | None, owner_epoch: int | None = None) -> dict[str, Any]:
231
- receiver = {
232
- "mode": "direct_tmux",
233
- "status": "attached",
234
- "provider": provider,
235
- "pane_id": target["pane_id"],
236
- "session_name": target["session_name"],
237
- "window_index": str(target["window_index"]),
238
- "window_name": target["window_name"],
239
- "pane_index": str(target["pane_index"]),
240
- "pane_tty": target["pane_tty"],
241
- "pane_current_command": target["pane_current_command"],
242
- "fingerprint": target.get("fingerprint") or _target_fingerprint(target),
243
- "attached_at": datetime.now(timezone.utc).isoformat(),
244
- }
245
- if leader_uuid:
246
- receiver["leader_session_uuid"] = leader_uuid
247
- if owner_epoch is not None:
248
- receiver["owner_epoch"] = owner_epoch
249
- return receiver
250
-
251
-
252
- def _broadcast_ambiguous_candidates(
253
- receiver: dict[str, Any],
254
- provider: str,
255
- candidates: list[dict[str, Any]],
256
- event_log: EventLog,
257
- owner_identity: dict[str, Any],
258
- team_id: str | None,
259
- ) -> dict[str, Any]:
260
- candidate_ids = sorted(str(candidate.get("pane_id")) for candidate in candidates)
261
- bucket = _ambiguous_debounce_bucket()
262
- incident_id = hashlib.sha256("\0".join([str(team_id or ""), *candidate_ids, bucket]).encode("utf-8")).hexdigest()[:16]
263
- if any(event.get("event") == "leader_receiver.ambiguous_candidates" and event.get("incident_id") == incident_id for event in event_log.tail(200)):
264
- return {"incident_id": incident_id, "deduped": True}
265
- prompt = _ambiguous_candidate_prompt(team_id, len(candidates))
266
- event_log.write(
267
- "leader_receiver.ambiguous_candidates",
268
- incident_id=incident_id,
269
- old_pane_id=receiver.get("pane_id"),
270
- candidates=candidate_ids,
271
- provider=provider,
272
- team_id=team_id,
273
- uuid_prefix=_uuid_prefix(owner_identity),
274
- debounce_bucket=bucket,
275
- # C16/C22: two or more live candidates remain; each must explicitly claim
276
- # with --confirm, so the broadcast carries the closed-enum lease reason.
277
- reason="force_confirm_required",
278
- )
279
- for candidate in candidates:
280
- pane_id = str(candidate.get("pane_id") or "")
281
- injected = _tmux_inject_text(
282
- pane_id,
283
- prompt,
284
- "Enter",
285
- f"team-agent-leader-ambiguous-{incident_id}-{pane_id.strip('%')}",
286
- provider=provider,
287
- )
288
- event_log.write(
289
- "leader_receiver.ambiguous_candidate_queued",
290
- incident_id=incident_id,
291
- pane_id=pane_id,
292
- ok=bool(injected.get("ok")),
293
- error=injected.get("error"),
294
- )
295
- return {"incident_id": incident_id, "deduped": False}
296
-
297
-
298
- def _ambiguous_debounce_bucket() -> str:
299
- now = datetime.now(timezone.utc)
300
- epoch = int(now.timestamp() // _AMBIGUOUS_DEBOUNCE_SECONDS) * _AMBIGUOUS_DEBOUNCE_SECONDS
301
- return datetime.fromtimestamp(epoch, timezone.utc).isoformat()
302
-
303
-
304
- def _ambiguous_candidate_prompt(team_id: str | None, candidate_count: int) -> str:
305
- others = max(candidate_count - 1, 0)
306
- return (
307
- f"Team `{team_id or 'current'}` has no bound leader. This window and {others} other window(s) all qualify. "
308
- "To claim this window as the team leader, run: `team-agent claim-leader --confirm`. "
309
- "Only the first such call wins; subsequent calls from other windows will be refused."
310
- )
311
-
312
-
313
- def _rediscovered_receiver(
314
- receiver: dict[str, Any],
315
- provider: str,
316
- target: dict[str, Any],
317
- event_log: EventLog,
318
- owner_identity: dict[str, Any] | None,
319
- invalidation_reason: str | None = None,
320
- ) -> dict[str, Any]:
321
- leader_uuid = _target_leader_session_uuid(target) or (owner_identity or {}).get("leader_session_uuid") or receiver.get("leader_session_uuid")
322
- updated = _receiver_from_target(target, provider, leader_uuid)
323
- updated["discovery"] = "stale_rediscovery_owner_identity" if owner_identity else "stale_rediscovery_unique_candidate"
324
- event_log.write(
325
- "leader_receiver.rediscovered",
326
- provider=provider,
327
- old_target=receiver.get("pane_id"),
328
- new_target=updated["pane_id"],
329
- candidate_count=1,
330
- owner_identity=owner_identity,
331
- )
332
- event_log.write(
333
- "leader_receiver.rebind_applied",
334
- old_pane_id=receiver.get("pane_id"),
335
- new_pane_id=updated["pane_id"],
336
- reason=invalidation_reason,
337
- owner_identity=owner_identity,
338
- uuid_prefix=_uuid_prefix(owner_identity),
339
- )
340
- return {"status": "updated", "receiver": updated, "owner_identity": owner_identity}
341
-
342
-
343
- def _validate_leader_receiver(receiver: dict[str, Any]) -> dict[str, Any]:
344
- pane_info = _legacy._tmux_pane_info(receiver.get("pane_id"))
345
- if not pane_info:
346
- return {"ok": False, "reason": "leader_pane_missing", "error": "tmux pane does not exist"}
347
- provider = str(receiver.get("provider") or "codex")
348
- if not _leader_command_looks_usable(pane_info.get("pane_current_command", ""), provider):
349
- return {
350
- "ok": False,
351
- "reason": "leader_pane_wrong_command",
352
- "error": f"pane command {pane_info.get('pane_current_command')!r} is not a leader host",
353
- "pane": pane_info,
354
- }
355
- expected_uuid = receiver.get("leader_session_uuid")
356
- if expected_uuid and _target_leader_session_uuid(pane_info):
357
- actual_uuid = _leader_uuid_for_bound_pane(receiver, pane_info)
358
- if not actual_uuid:
359
- return {"ok": False, "reason": "leader_uuid_missing", "error": "bound pane has no TEAM_AGENT_LEADER_SESSION_UUID", "pane": pane_info}
360
- if actual_uuid != expected_uuid:
361
- return {
362
- "ok": False,
363
- "reason": "leader_uuid_mismatch",
364
- "error": "bound pane TEAM_AGENT_LEADER_SESSION_UUID does not match stored team owner",
365
- "pane": pane_info,
366
- }
367
- capture = run_cmd(["tmux", "capture-pane", "-p", "-S", "-40", "-t", pane_info["pane_id"]], timeout=5)
368
- if capture.returncode != 0:
369
- return {
370
- "ok": False,
371
- "reason": "leader_capture_failed",
372
- "error": capture.stderr.strip() or "tmux capture-pane failed",
373
- "pane": pane_info,
374
- }
375
- return {"ok": True, "pane": pane_info, "capture": capture.stdout, "warning": None}
376
-
377
-
378
- def _leader_command_looks_usable(command: str, provider: str) -> bool:
379
- _ = provider
380
- return bool(str(command or "").strip())
381
-
382
-
383
- def attempt_trust_auto_answer(
384
- workspace: Path,
385
- pane_id: str | None,
386
- pane_capture_tail: str,
387
- event_log: EventLog,
388
- *,
389
- spec: dict[str, Any] | None = None,
390
- state: dict[str, Any] | None = None,
391
- ) -> dict[str, Any]:
392
- """Auto-answer Codex trust only when the prompt path is exactly this workspace."""
393
- if spec is None and state is not None:
394
- spec_path_str = state.get("spec_path")
395
- if spec_path_str:
396
- try:
397
- from team_agent.spec import load_spec as _load_spec
398
- spec = _load_spec(Path(spec_path_str))
399
- except Exception:
400
- spec = None
401
- explicit_opt_in = _auto_trust_opt_in(spec, event_log=event_log)
402
- runtime_cfg = spec.get("runtime") if isinstance(spec, dict) else None
403
- implicit_own_workspace_trust = (
404
- (spec is None and (state is None or ("agents" not in state and "session_name" not in state)))
405
- or (spec is None and str(pane_id or "").startswith("%"))
406
- or (isinstance(state, dict) and bool(state.get("workspace_root") or state.get("trust_auto_answer_stage")))
407
- or isinstance(runtime_cfg, dict)
408
- )
409
- if not implicit_own_workspace_trust and not explicit_opt_in:
410
- event_log.write(
411
- "leader_panes.trust_auto_answer_skipped",
412
- pane_id=pane_id,
413
- workspace=str(workspace),
414
- reason="not_opted_in",
415
- )
416
- return {"ok": False, "answered": False, "reason": "not_opted_in"}
417
- if not pane_id:
418
- event_log.write(
419
- "leader_panes.trust_auto_answer_skipped",
420
- pane_id=None,
421
- workspace=str(workspace),
422
- reason="pane_id_missing",
423
- )
424
- return {"ok": False, "answered": False, "reason": "pane_id_missing"}
425
- capture_hash = hashlib.sha256(pane_capture_tail.encode("utf-8")).hexdigest()
426
- idempotency_key = (str(pane_id), capture_hash)
427
- if idempotency_key in _TRUST_AUTO_ANSWERED:
428
- return {"ok": True, "answered": True, "reason": "already_answered", "action": "already_answered"}
429
- pane_width = state.get("pane_width") if explicit_opt_in and isinstance(state, dict) else None
430
- if not _capture_tail_references_workspace(pane_capture_tail, workspace, pane_width):
431
- event_log.write(
432
- "leader_panes.trust_auto_answer_refused",
433
- pane_id=pane_id,
434
- workspace=str(workspace),
435
- reason="workspace_dir_mismatch",
436
- action="prompt_leader",
437
- )
438
- return {
439
- "ok": False,
440
- "answered": False,
441
- "reason": "workspace_dir_mismatch",
442
- "action": "prompt_leader",
443
- "next_step": "Ask the leader whether to trust this foreign workspace prompt.",
444
- }
445
- answer = _tmux_inject_text(
446
- str(pane_id),
447
- "" if explicit_opt_in else "1",
448
- "Enter",
449
- f"team-agent-trust-auto-answer-{str(pane_id).strip('%') or 'pane'}",
450
- attempts=1,
451
- provider="fake",
452
- bypass_non_input_gate=True,
453
- )
454
- if not answer.get("ok"):
455
- error = answer.get("error") or "tmux send-keys failed"
456
- event_log.write(
457
- "leader_panes.trust_auto_answer_failed",
458
- pane_id=pane_id,
459
- workspace=str(workspace),
460
- error=error,
461
- )
462
- return {"ok": False, "answered": False, "reason": "tmux_send_keys_failed", "error": error}
463
- _TRUST_AUTO_ANSWERED.add(idempotency_key)
464
- event_log.write(
465
- "leader_panes.trust_auto_answered",
466
- pane_id=pane_id,
467
- workspace=str(workspace),
468
- capture_hash=capture_hash,
469
- )
470
- return {"ok": True, "answered": True, "reason": "trust_auto_answered"}
471
-
472
-
473
- _SPEC_OPT_IN_DEPRECATION_MESSAGE = (
474
- "WARNING: spec.runtime.auto_trust_own_workspace is deprecated. "
475
- "Use env TEAM_AGENT_AUTO_TRUST_OWN_WORKSPACE=1 per session instead. "
476
- "Spec-field will be removed in 0.3.0."
477
- )
478
-
479
-
480
- def _auto_trust_opt_in(spec: dict[str, Any] | None, *, event_log: EventLog | None = None) -> bool:
481
- """Constitution-reviewer F3 (2026-05-26): env-var per-session opt-in is the
482
- preferred path. spec.runtime.auto_trust_own_workspace remains honoured for
483
- backwards compatibility but emits a one-shot stderr deprecation warning AND
484
- a structured trust_auto_answer_spec_opt_in_deprecated event so a normalized
485
- YAML field is auditable from a fresh log."""
486
- spec_opted_in = (
487
- isinstance(spec, dict)
488
- and bool((spec.get("runtime") or {}).get("auto_trust_own_workspace"))
489
- )
490
- if spec_opted_in:
491
- _emit_spec_opt_in_deprecation(event_log)
492
- env = os.environ.get("TEAM_AGENT_AUTO_TRUST_OWN_WORKSPACE", "").strip().lower()
493
- env_opted_in = env in {"1", "true", "yes", "on"}
494
- return env_opted_in or spec_opted_in
495
-
496
-
497
- def _emit_spec_opt_in_deprecation(event_log: EventLog | None) -> None:
498
- """Emit the deprecation warning once per process. The structured event still
499
- fires per call so an audit log captures every yaml-driven decision."""
500
- import sys
501
- global _SPEC_OPT_IN_DEPRECATION_WARNED
502
- if not _SPEC_OPT_IN_DEPRECATION_WARNED:
503
- try:
504
- print(_SPEC_OPT_IN_DEPRECATION_MESSAGE, file=sys.stderr, flush=True)
505
- except Exception:
506
- pass
507
- _SPEC_OPT_IN_DEPRECATION_WARNED = True
508
- if event_log is not None:
509
- try:
510
- event_log.write(
511
- "trust_auto_answer_spec_opt_in_deprecated",
512
- preferred_opt_in="env:TEAM_AGENT_AUTO_TRUST_OWN_WORKSPACE",
513
- deprecated_field="spec.runtime.auto_trust_own_workspace",
514
- removal_target_version="0.3.0",
515
- )
516
- except Exception:
517
- pass
518
-
519
-
520
- _SPEC_OPT_IN_DEPRECATION_WARNED = False
521
- _TRUST_AUTO_ANSWERED: set[tuple[str, str]] = set()
522
-
523
-
524
- def _reset_spec_opt_in_deprecation_state() -> None:
525
- """Test-only helper: reset the per-process one-shot guard so multiple cases
526
- in the same interpreter can each observe the warning. Not part of the
527
- public API."""
528
- global _SPEC_OPT_IN_DEPRECATION_WARNED
529
- _SPEC_OPT_IN_DEPRECATION_WARNED = False
530
-
531
-
532
- def _capture_tail_references_workspace(tail: str, workspace: Path, pane_width: int | None = None) -> bool:
533
- """Decide whether the Codex trust-prompt tail names the worker's own
534
- workspace cwd. The runtime cwd is the source of truth; the prompt path is a
535
- consistency guard. Match cases (one converged helper per token):
536
-
537
- - exact canonical equality (the unchanged baseline);
538
- - mid-ellipsis ``head…tail`` / ``head...tail`` where head is a prefix of
539
- the runtime cwd and tail is its suffix;
540
- - hard right-edge truncation: the canonical runtime cwd starts with the
541
- canonical captured path AND the captured token reaches the capture
542
- line's right boundary (pane_width).
543
-
544
- Without a pane_width signal, prefix matching is forbidden — the captured
545
- path is treated as a complete token and must exactly equal the runtime cwd
546
- (this is what stops ``/repo`` from sliding into ``/repo-backup``).
547
- """
548
- if not tail:
549
- return False
550
- workspace_canonical = _canonicalize_path(workspace)
551
- if not workspace_canonical:
552
- return False
553
- for token, source_line in _candidate_path_lines_from_prompt(tail):
554
- if _workspace_matches_token(workspace_canonical, token, source_line, pane_width):
555
- return True
556
- return False
557
-
558
-
559
- _PATH_LINE_RE = re.compile(r"(/[\w\-./~+@…]+)")
560
- _ELLIPSIS_TOKENS = ("…", "...")
561
-
562
-
563
- def _candidate_path_lines_from_prompt(tail: str) -> list[tuple[str, str]]:
564
- """Pull (path_token, source_line) pairs out of the prompt's tail. The
565
- source line is the line AFTER stripping Codex box-drawing glyphs, so the
566
- matcher can locate the token's end column relative to the visible width."""
567
- pairs: list[tuple[str, str]] = []
568
- seen: set[tuple[str, str]] = set()
569
- for raw_line in tail.splitlines():
570
- line = raw_line.strip()
571
- for glyph in ("▌", "▎", "│"):
572
- line = line.lstrip(glyph).strip()
573
- if not line:
574
- continue
575
- for match in _PATH_LINE_RE.finditer(line):
576
- token = match.group(1).rstrip("/")
577
- if not token:
578
- continue
579
- key = (token, line)
580
- if key in seen:
581
- continue
582
- seen.add(key)
583
- pairs.append(key)
584
- return pairs
585
-
586
-
587
- def _candidate_paths_from_prompt(tail: str) -> list[str]:
588
- """Backwards-compatible token-only view (kept for any external callers)."""
589
- out: list[str] = []
590
- for token, _line in _candidate_path_lines_from_prompt(tail):
591
- if token not in out:
592
- out.append(token)
593
- return out
594
-
595
-
596
- def _workspace_matches_token(
597
- workspace_canonical: str,
598
- token: str,
599
- source_line: str,
600
- pane_width: int | None,
601
- ) -> bool:
602
- """The converged trust-prompt match logic.
603
-
604
- Order matters:
605
- 1. exact canonical equality;
606
- 2. mid-ellipsis head/tail match;
607
- 3. right-edge hard truncation (prefix + boundary-reached).
608
- A captured token that does NOT reach the line's right boundary is treated
609
- as a complete short path and must equal the runtime cwd exactly.
610
- """
611
- # 1. Exact canonical equality.
612
- captured_canonical = _canonicalize_path(Path(token))
613
- if not captured_canonical:
614
- return False
615
- if captured_canonical == workspace_canonical:
616
- return True
617
- # 2. Mid-ellipsis: split on … or ..., require head ⊑ workspace and workspace ⊐ tail.
618
- for ellipsis in _ELLIPSIS_TOKENS:
619
- if ellipsis in token:
620
- head, _, tail_part = token.partition(ellipsis)
621
- head_canonical = _canonicalize_path(Path(head)) if head.startswith("/") else head
622
- if not head_canonical or not tail_part:
623
- return False
624
- return (
625
- workspace_canonical.startswith(head_canonical)
626
- and workspace_canonical.endswith(tail_part)
627
- )
628
- # 3. Right-edge hard truncation: prefix + boundary.
629
- if not _token_reaches_right_edge(token, source_line, pane_width):
630
- # No boundary signal → captured must be a complete token; exact already
631
- # failed → mismatch (this rejects /repo vs /repo-backup both ways).
632
- return False
633
- return (
634
- workspace_canonical == captured_canonical
635
- or workspace_canonical.startswith(captured_canonical + "/")
636
- or workspace_canonical.startswith(captured_canonical)
637
- )
638
-
639
-
640
- def _token_reaches_right_edge(token: str, source_line: str, pane_width: int | None) -> bool:
641
- """The token reaches the capture line's right boundary iff the line is wide
642
- enough to be at pane capacity AND the token sits flush against the line's
643
- end. Without a pane_width we cannot prove truncation — return False so the
644
- caller falls back to exact-equality (this is the C/repo vs C/repo-backup
645
- safeguard)."""
646
- if not pane_width or pane_width <= 0:
647
- return False
648
- rstripped = source_line.rstrip()
649
- if not rstripped.endswith(token):
650
- return False
651
- # Allow a one-column tolerance for trailing whitespace stripped from the
652
- # raw capture; the line must be at pane capacity to count as hard-cut.
653
- return len(rstripped) >= max(1, pane_width - 1)
654
-
655
-
656
- def _canonicalize_path(p: Path | str) -> str:
657
- try:
658
- resolved = Path(p).expanduser().resolve(strict=False)
659
- except OSError:
660
- return ""
661
- text = resolved.as_posix()
662
- # Strip a trailing slash so boundary-safe equality holds.
663
- return text.rstrip("/") if text != "/" else "/"
664
-
665
-
666
- def _choose_leader_submit_key(provider: str, capture_text: str) -> tuple[str, str]:
667
- if provider != "codex":
668
- return "Enter", "non_codex_provider"
669
- if re.search(r"esc to interrupt|working|running", capture_text, re.IGNORECASE):
670
- return "Enter", "codex_busy_submit_followup"
671
- if re.search(r"(›|❯|codex>)", capture_text):
672
- return "Enter", "codex_idle_prompt"
673
- return "Enter", "codex_state_unknown_submit"