@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,437 @@
1
+ use super::*;
2
+ use crate::transport::test_support::OfflineTransport;
3
+
4
+ // ═══════════════ P2 FIX-LOOP RED (复绿即对抗 cross-model findings) ═══════════════
5
+ // Golden re-probed via /tmp/probe_p2b_msg.py vs team-agent-public @ 439bef8.
6
+
7
+ // P0 — IRON LAW (bug-064/082): trust auto-answer must REFUSE a prompt whose path is a
8
+ // SUBDIRECTORY or SIBLING of the workspace. The current substring `.contains()` answers
9
+ // both (a subdir/sibling string contains the workspace as a substring). leader_panes.py
10
+ // requires EXACT canonical equality.
11
+ #[test]
12
+ fn p2_trust_refuses_subdirectory_and_sibling_of_workspace() {
13
+ let ws = tmp_ws("trustsubsib");
14
+ let canonical = std::fs::canonicalize(&ws).unwrap();
15
+ let log = EventLog::new(&ws);
16
+ let t = NoopTransport;
17
+ let pane = PaneId::new("%7");
18
+ let canon = canonical.to_string_lossy().to_string();
19
+
20
+ let subdir = format!("Allow Codex to write to {canon}/subproject ?");
21
+ let s = attempt_trust_auto_answer(
22
+ &canonical, &t, Some(&pane), &subdir, &PaneWidthQuery::Ok { pane_width: 200 }, &log,
23
+ )
24
+ .unwrap();
25
+ assert!(!s.answered, "a SUBDIRECTORY of the workspace must NOT auto-answer trust");
26
+ assert_eq!(s.reason, "workspace_dir_mismatch");
27
+
28
+ let sibling = format!("Allow Codex to write to {canon}-backup ?");
29
+ let sib = attempt_trust_auto_answer(
30
+ &canonical, &t, Some(&pane), &sibling, &PaneWidthQuery::Ok { pane_width: 200 }, &log,
31
+ )
32
+ .unwrap();
33
+ assert!(!sib.answered, "a SIBLING (<ws>-backup) must NOT auto-answer trust");
34
+ assert_eq!(sib.reason, "workspace_dir_mismatch");
35
+ }
36
+
37
+ // P1 — owner-gate worker bypass: sender == custom leader_id (state.leader.id) or the
38
+ // 'Leader' literal must NOT bypass (state.py worker_sender_bypasses_owner_gate).
39
+ #[test]
40
+ fn p2_owner_bypass_rejects_custom_leader_id_and_capital_leader() {
41
+ let ws = tmp_ws("bypassid");
42
+ let log = EventLog::new(&ws);
43
+ let target = MessageTarget::Single("leader".to_string());
44
+
45
+ let s1 = serde_json::json!({"leader":{"id":"boss"},"agents":{"boss":{}}});
46
+ assert!(
47
+ !apply_worker_sender_bypass(&s1, Some("boss"), &target, None, &log).unwrap(),
48
+ "sender == custom leader id must not bypass the owner gate"
49
+ );
50
+ let s2 = serde_json::json!({"agents":{"Leader":{}}});
51
+ assert!(
52
+ !apply_worker_sender_bypass(&s2, Some("Leader"), &target, None, &log).unwrap(),
53
+ "'Leader' literal must not bypass"
54
+ );
55
+ }
56
+
57
+ // P1 — owner-gate bypass honors the TEAM_AGENT_ID env identity gate: set and != sender → deny.
58
+ #[test]
59
+ #[serial_test::serial(env)]
60
+ fn p2_owner_bypass_denies_on_env_agent_id_mismatch() {
61
+ let _g = ENV_LOCK_MSG.lock().unwrap_or_else(|p| p.into_inner());
62
+ let _e = EnvGuardMsg::set("TEAM_AGENT_ID", Some("other"));
63
+ let ws = tmp_ws("bypassenv");
64
+ let log = EventLog::new(&ws);
65
+ let target = MessageTarget::Single("leader".to_string());
66
+ let s = serde_json::json!({"agents":{"w1":{}}});
67
+ assert!(
68
+ !apply_worker_sender_bypass(&s, Some("w1"), &target, None, &log).unwrap(),
69
+ "TEAM_AGENT_ID set and != sender must deny the bypass"
70
+ );
71
+ }
72
+
73
+ // P1 — classify_agent_activity must read current_command, stale last_output, multiline /
74
+ // Codex idle prompts, and Thinking/lowercase-working (activity_detector.py:90-146).
75
+ #[test]
76
+ fn p2_classify_activity_reads_command_stale_and_prompts() {
77
+ let st = serde_json::json!({});
78
+ // (1) non-provider current_command → uncertain 0.75 (current drops command → 0.5).
79
+ let a = classify_agent_activity(&st, "", false, Some("vim"), None);
80
+ assert_eq!((a.status, a.confidence), (ActivityStatus::Uncertain, 0.75));
81
+ // (2) stale last_output (≥ stuck_timeout) → stuck 0.85.
82
+ let stale = (chrono::Utc::now() - chrono::Duration::seconds(400)).to_rfc3339();
83
+ let b = classify_agent_activity(&st, "", false, None, Some(&stale));
84
+ assert_eq!((b.status, b.confidence), (ActivityStatus::Stuck, 0.85));
85
+ // (3) embedded multiline idle prompt (Codex ❯ not on its own trimmed line) → idle 0.9.
86
+ let c = classify_agent_activity(&st, "some line\n❯\nmore", false, None, None);
87
+ assert_eq!((c.status, c.confidence), (ActivityStatus::Idle, 0.9));
88
+ // (4) 'Thinking' working indicator → working 0.9.
89
+ let d = classify_agent_activity(&st, "Thinking about it", false, None, None);
90
+ assert_eq!((d.status, d.confidence), (ActivityStatus::Working, 0.9));
91
+ // (5) lowercase 'working' indicator → working 0.9.
92
+ let e = classify_agent_activity(&st, "working on it", false, None, None);
93
+ assert_eq!((e.status, e.confidence), (ActivityStatus::Working, 0.9));
94
+ }
95
+
96
+ // P1 — scheduler dispatch must be exhaustive: an unknown kind surfaces an error, not a
97
+ // silent {ok:false} (scheduler.py: 'unknown scheduled event kind: <kind>').
98
+ #[test]
99
+ fn p2_scheduler_unknown_kind_surfaces_error() {
100
+ let ws = tmp_ws("schedunknown");
101
+ let store = store_for(&ws);
102
+ let conn = crate::db::schema::open_db(store.db_path()).unwrap();
103
+ conn.execute(
104
+ "insert into scheduled_events(owner_team_id, due_at, target, kind, payload_json, status, created_at) \
105
+ values (null, '2000-01-01T00:00:00+00:00', 't', 'bogus_kind', '{}', 'pending', '2000-01-01T00:00:00+00:00')",
106
+ [],
107
+ )
108
+ .unwrap();
109
+ let log = EventLog::new(&ws);
110
+ let r = fire_due_scheduled_events(&ws, &store, &NoopTransport, &log);
111
+ assert!(r.is_err(), "an unknown scheduled event kind must surface an error, not a silent ok=false");
112
+ }
113
+
114
+ // ═════════════════════════════════════════════════════════════════════════
115
+ // SPINE-WIRING (③ review→fix) RED — deliver_pending_messages + scheduler divergences
116
+ // vs golden v0.2.11 (messaging/delivery.py + scheduler.py + core.py). Probes in
117
+ // /tmp/spine_divergences.md (#1,#2,#6).
118
+ // ═════════════════════════════════════════════════════════════════════════
119
+
120
+ /// A delivery transport whose `inject` RECORDS-and-succeeds (the real deliver loop legitimately
121
+ /// injects; the §84-guard NoopTransport panics). Only `inject` is reached by deliver_pending_message.
122
+ struct DeliverOkTransport;
123
+ impl Transport for DeliverOkTransport {
124
+ fn kind(&self) -> BackendKind {
125
+ BackendKind::Tmux
126
+ }
127
+ fn spawn_first(&self, _s: &SessionName, _w: &WindowName, _a: &[String], _c: &Path, _e: &BTreeMap<String, String>) -> Result<SpawnResult, TransportError> {
128
+ unimplemented!("not reached in delivery")
129
+ }
130
+ fn spawn_into(&self, _s: &SessionName, _w: &WindowName, _a: &[String], _c: &Path, _e: &BTreeMap<String, String>) -> Result<SpawnResult, TransportError> {
131
+ unimplemented!("not reached in delivery")
132
+ }
133
+ fn inject(&self, _t: &Target, _p: &InjectPayload, _s: Key, _b: bool) -> Result<InjectReport, TransportError> {
134
+ Ok(InjectReport {
135
+ stage_reached: crate::transport::InjectStage::Submit,
136
+ inject_verification: crate::transport::InjectVerification::CaptureContainsToken,
137
+ submit_verification: crate::transport::SubmitVerification::EnterSentWithoutPlaceholderCheck,
138
+ turn_verification: crate::transport::TurnVerification::NotYetObserved,
139
+ attempts: 1,
140
+ })
141
+ }
142
+ fn send_keys(&self, _t: &Target, _k: &[Key]) -> Result<(), TransportError> {
143
+ Ok(())
144
+ }
145
+ fn capture(&self, _t: &Target, range: CaptureRange) -> Result<CapturedText, TransportError> {
146
+ Ok(CapturedText { text: String::new(), range })
147
+ }
148
+ fn query(&self, _t: &Target, _f: PaneField) -> Result<Option<String>, TransportError> {
149
+ Ok(None)
150
+ }
151
+ fn liveness(&self, _p: &PaneId) -> Result<PaneLiveness, TransportError> {
152
+ Ok(PaneLiveness::Unknown)
153
+ }
154
+ fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
155
+ Ok(Vec::new())
156
+ }
157
+ fn has_session(&self, _s: &SessionName) -> Result<bool, TransportError> {
158
+ Ok(true)
159
+ }
160
+ fn list_windows(&self, _s: &SessionName) -> Result<Vec<WindowName>, TransportError> {
161
+ Ok(Vec::new())
162
+ }
163
+ fn set_session_env(&self, _s: &SessionName, _k: &str, _v: &str) -> Result<SetEnvOutcome, TransportError> {
164
+ Ok(SetEnvOutcome::Applied)
165
+ }
166
+ fn kill_session(&self, _s: &SessionName) -> Result<(), TransportError> {
167
+ Ok(())
168
+ }
169
+ fn kill_window(&self, _t: &Target) -> Result<(), TransportError> {
170
+ Ok(())
171
+ }
172
+ fn attach_session(&self, _s: &SessionName) -> Result<AttachOutcome, TransportError> {
173
+ Ok(AttachOutcome::Attached)
174
+ }
175
+ }
176
+
177
+ fn set_message_status(store: &MessageStore, message_id: &str, status: &str) {
178
+ let conn = crate::db::schema::open_db(store.db_path()).unwrap();
179
+ conn.execute("update messages set status = ?2 where message_id = ?1", sql_params![message_id, status]).unwrap();
180
+ }
181
+ fn message_status_of(store: &MessageStore, message_id: &str) -> String {
182
+ let conn = crate::db::schema::open_db(store.db_path()).unwrap();
183
+ conn.query_row("select status from messages where message_id = ?1", sql_params![message_id], |r| r.get::<_, String>(0)).unwrap()
184
+ }
185
+ fn scheduled_status_of(store: &MessageStore, id: i64) -> String {
186
+ let conn = crate::db::schema::open_db(store.db_path()).unwrap();
187
+ conn.query_row("select status from scheduled_events where id = ?1", [id], |r| r.get::<_, String>(0)).unwrap()
188
+ }
189
+ fn seed_agent_health(store: &MessageStore, agent_id: &str, status: &str) {
190
+ let conn = crate::db::schema::open_db(store.db_path()).unwrap();
191
+ conn.execute(
192
+ "insert into agent_health(owner_team_id, agent_id, status, last_output_at, context_usage_pct, current_task_id, updated_at) \
193
+ values (null, ?1, ?2, null, null, null, ?3)",
194
+ sql_params![agent_id, status, chrono::Utc::now().to_rfc3339()],
195
+ )
196
+ .unwrap();
197
+ }
198
+ fn seed_event_due(store: &MessageStore, kind: &str, due_at: &str, payload: &str) -> i64 {
199
+ let conn = crate::db::schema::open_db(store.db_path()).unwrap();
200
+ conn.execute(
201
+ "insert into scheduled_events(owner_team_id, due_at, target, kind, payload_json, status, created_at) \
202
+ values (null, ?1, 't', ?2, ?3, 'pending', ?1)",
203
+ sql_params![due_at, kind, payload],
204
+ )
205
+ .unwrap();
206
+ conn.last_insert_rowid()
207
+ }
208
+ fn read_event_log(ws: &Path) -> Vec<serde_json::Value> {
209
+ let path = crate::model::paths::logs_dir(ws).join("events.jsonl");
210
+ match std::fs::read_to_string(&path) {
211
+ Ok(text) => text.lines().filter_map(|l| serde_json::from_str(l).ok()).collect(),
212
+ Err(_) => Vec::new(),
213
+ }
214
+ }
215
+
216
+ // #1 — deliver_pending_messages delivers ONLY {pending,accepted}; queued_* rows are scheduler-owned
217
+ // and must be left untouched (golden delivery.py:484 `if row["status"] not in {"pending","accepted"}: continue`).
218
+ #[test]
219
+ fn spine_delivery_skips_queued_statuses() {
220
+ let ws = tmp_ws("deliver-queued");
221
+ let store = store_for(&ws);
222
+ let log = EventLog::new(&ws);
223
+ let _acc = store.create_message(Some("t"), "leader", "w1", "hi", None, true, None).unwrap();
224
+ let qidle = store.create_message(Some("t"), "leader", "w1", "hi", None, true, None).unwrap();
225
+ let qstart = store.create_message(Some("t"), "leader", "w1", "hi", None, true, None).unwrap();
226
+ set_message_status(&store, &qidle, "queued_until_idle");
227
+ set_message_status(&store, &qstart, "queued_until_start");
228
+ let state = serde_json::json!({"agents": {"w1": {}}});
229
+
230
+ let _ = deliver_pending_messages(&ws, &state, &DeliverOkTransport, &log).unwrap();
231
+
232
+ assert_eq!(message_status_of(&store, &qidle), "queued_until_idle", "queued_until_idle must NOT be claimed/injected/marked by deliver_pending");
233
+ assert_eq!(message_status_of(&store, &qstart), "queued_until_start", "queued_until_start must NOT be touched by deliver_pending");
234
+ }
235
+
236
+ // #2 (CONTRACT, corrected) — busy recipient = LIFECYCLE status=="busy" → emit ONE send.deferred_busy
237
+ // {recipient,reason:"recipient_busy"} and do NOT deliver (row stays 'accepted'). golden delivery.py:491
238
+ // gates on `state.agents[recipient].status == "busy"`, NOT on turn-level agent_health=WORKING.
239
+ // (The prior version of this test seeded agent_health=WORKING + status="running" and asserted deferral,
240
+ // misreading golden — that stale lock is the root of the deferred_busy regression; corrected here.)
241
+ #[test]
242
+ fn spine_delivery_busy_recipient_defers_with_event() {
243
+ let ws = tmp_ws("deliver-busy");
244
+ let store = store_for(&ws);
245
+ let log = EventLog::new(&ws);
246
+ let mid = store.create_message(Some("t"), "leader", "w1", "hi", None, true, None).unwrap();
247
+ let state = serde_json::json!({"agents": {"w1": {"status": "busy"}}});
248
+
249
+ let delivered = deliver_pending_messages(&ws, &state, &DeliverOkTransport, &log).unwrap();
250
+
251
+ assert!(!delivered.contains(&mid), "a busy recipient's message must NOT be delivered");
252
+ assert_eq!(message_status_of(&store, &mid), "accepted", "the row must stay 'accepted' (no-drop, left for a later tick)");
253
+ let events = read_event_log(&ws);
254
+ let deferred: Vec<_> = events
255
+ .iter()
256
+ .filter(|e| e.get("event").and_then(|v| v.as_str()) == Some("send.deferred_busy"))
257
+ .collect();
258
+ assert_eq!(deferred.len(), 1, "exactly one send.deferred_busy event expected; got {events:?}");
259
+ assert_eq!(deferred[0].get("recipient").and_then(|v| v.as_str()), Some("w1"));
260
+ assert_eq!(deferred[0].get("reason").and_then(|v| v.as_str()), Some("recipient_busy"));
261
+ }
262
+
263
+ // CONTRACT (shared-root, real-machine-driven; golden = correct-behavior baseline): an ALIVE worker
264
+ // (lifecycle status="running") must be DELIVERABLE even when its turn-level agent_health=WORKING.
265
+ // golden's busy gate is state.agents[recipient].status=="busy" (delivery.py:491) and status is NEVER
266
+ // "busy" for an alive worker (only running/stopped), so golden never defers an alive worker. Rust
267
+ // recipient_is_busy (delivery.rs:291) reads `agent_health WHERE status='WORKING'` — turn-level
268
+ // idle-detection state — and defers. This is the Stage-B / stop-reset deferred_busy regression root:
269
+ // turn-level agent_health must NOT gate delivery; lifecycle status (alive) does.
270
+ #[test]
271
+ fn contract_alive_worker_with_working_health_is_deliverable_not_deferred() {
272
+ let ws = tmp_ws("deliver-alive-working");
273
+ let store = store_for(&ws);
274
+ let log = EventLog::new(&ws);
275
+ let mid = store.create_message(Some("t"), "leader", "w1", "hi", None, true, None).unwrap();
276
+ seed_agent_health(&store, "w1", "WORKING"); // turn-level state — must NOT gate delivery
277
+ let state = serde_json::json!({"agents": {"w1": {"status": "running"}}}); // lifecycle: alive
278
+
279
+ let delivered = deliver_pending_messages(&ws, &state, &DeliverOkTransport, &log).unwrap();
280
+
281
+ let events = read_event_log(&ws);
282
+ let deferred: Vec<_> = events
283
+ .iter()
284
+ .filter(|e| e.get("event").and_then(|v| v.as_str()) == Some("send.deferred_busy"))
285
+ .collect();
286
+ assert!(
287
+ deferred.is_empty(),
288
+ "CONTRACT: an alive worker (lifecycle status=running) must NOT be deferred_busy on agent_health=WORKING; \
289
+ golden busy gate is lifecycle status=='busy' (never set for alive workers). got {deferred:?}"
290
+ );
291
+ assert!(
292
+ delivered.contains(&mid),
293
+ "the message to an alive worker must be delivered (round-trip), not deferred. delivered={delivered:?}"
294
+ );
295
+ }
296
+
297
+ // #6a — fire_due_scheduled_events marks 'done' if result.ok else 'failed' (scheduler.py:117).
298
+ // An EXHAUSTED trust_retry (attempt>=max) → handle_trust_retry_needed returns ok:false → 'failed'.
299
+ #[test]
300
+ fn spine_scheduler_marks_failed_when_result_not_ok() {
301
+ let ws = tmp_ws("sched-failed");
302
+ let store = store_for(&ws);
303
+ let log = EventLog::new(&ws);
304
+ let payload = serde_json::json!({"message_id": "msg_x", "attempt": 5, "max_attempts": 5, "first_target": "%1"}).to_string();
305
+ let id = seed_event_due(&store, "trust_retry", "2000-01-01T00:00:00+00:00", &payload);
306
+
307
+ let _ = fire_due_scheduled_events(&ws, &store, &NoopTransport, &log).unwrap();
308
+
309
+ assert_eq!(
310
+ scheduled_status_of(&store, id),
311
+ "failed",
312
+ "a not-ok result (exhausted trust_retry) must mark the scheduled event 'failed', not unconditional 'done'"
313
+ );
314
+ }
315
+
316
+ // #6b — due events fire in (due_at, id) order (core.py due_scheduled_events), not id order.
317
+ #[test]
318
+ fn spine_scheduler_orders_due_events_by_due_at_then_id() {
319
+ let ws = tmp_ws("sched-order");
320
+ let store = store_for(&ws);
321
+ let log = EventLog::new(&ws);
322
+ // smaller id is due LATER → due_at order must reverse the id order.
323
+ let a_late = seed_event_due(&store, "health_ping", "2000-01-02T00:00:00+00:00", "{}");
324
+ let b_early = seed_event_due(&store, "health_ping", "2000-01-01T00:00:00+00:00", "{}");
325
+
326
+ let fired = fire_due_scheduled_events(&ws, &store, &NoopTransport, &log).unwrap();
327
+
328
+ assert_eq!(
329
+ fired,
330
+ vec![b_early, a_late],
331
+ "fire order must be by (due_at, id) — earlier-due fires first; current orders by id only. got {fired:?}"
332
+ );
333
+ }
334
+
335
+ // ═════════════════════════════════════════════════════════════════════════
336
+ // rt-host-a LOOP #3 — coordinator deliver tick injects to the AGENT ID treated as a bare PANE-ID.
337
+ // delivery.rs:127 builds Target::Pane(PaneId::new(message.recipient)) where recipient is the agent id
338
+ // ("w1"). A coordinator process with NO attached tmux client cannot resolve a bare name -> every tick
339
+ // "can't find pane: w1" -> message stuck -> no delivery. The fix (porter/leader): resolve the recipient
340
+ // to a SESSION-QUALIFIED target (SessionWindow{session: state.session_name, window: agent.window}) OR
341
+ // the persisted state.agents[recipient].pane_id — mirroring coordinator/tick.rs::capture_target —
342
+ // NEVER Target::Pane(agent_id). 793/0/21 missed it: the deliver tests use a non-asserting transport.
343
+ // ═════════════════════════════════════════════════════════════════════════
344
+
345
+ // RED — deliver_pending_messages must inject to a RESOLVABLE target (session-qualified or a real
346
+ // pane-id), NOT the bare agent-id treated as a pane. Today delivery.rs:127 records
347
+ // Target::Pane(PaneId("w1")) -> a non-attached coordinator can't resolve it -> RED at assert_ne!.
348
+ #[test]
349
+ fn spine_delivery_injects_resolvable_target_not_bare_agent_pane() {
350
+ let ws = tmp_ws("deliver-target");
351
+ let store = store_for(&ws);
352
+ let log = EventLog::new(&ws);
353
+ let _mid = store.create_message(Some("t"), "leader", "w1", "hi", None, true, None).unwrap();
354
+ // in-team, non-busy agent -> deliver proceeds; the session-qualified resolution uses session_name + window.
355
+ let state = serde_json::json!({
356
+ "session_name": "team-x",
357
+ "agents": {"w1": {"status": "idle", "window": "w1"}}
358
+ });
359
+ let transport = OfflineTransport::new();
360
+
361
+ let _ = deliver_pending_messages(&ws, &state, &transport, &log).unwrap();
362
+
363
+ let recorded = transport.inject_targets();
364
+ assert_eq!(recorded.len(), 1, "deliver must inject the one pending message exactly once; got {recorded:?}");
365
+ let target = recorded[0].clone();
366
+ // THE BUG (rt-host-a #3): deliver builds Target::Pane(PaneId(message.recipient)) — the agent id as a
367
+ // BARE pane, which a non-attached coordinator can't resolve ("can't find pane: w1").
368
+ assert_ne!(
369
+ target,
370
+ Target::Pane(PaneId::new("w1")),
371
+ "deliver must NOT inject to the bare agent-id as a pane-id (a non-attached coordinator can't \
372
+ resolve it -> 'can't find pane: w1'); expected a SESSION-QUALIFIED target \
373
+ (SessionWindow{{session: team-x, window: w1}}) or the persisted pane-id, got {target:?}"
374
+ );
375
+ // ...and it must be a genuinely resolvable target: session-qualified (session=team-x) or a real
376
+ // pane-id (anything but the bare agent name).
377
+ let resolvable = match &target {
378
+ Target::SessionWindow { session, .. } => session.as_str() == "team-x",
379
+ Target::Pane(pane) => pane.as_str() != "w1",
380
+ };
381
+ assert!(
382
+ resolvable,
383
+ "deliver must resolve recipient 'w1' to session=team-x (session-qualified) or a real pane-id; got {target:?}"
384
+ );
385
+ }
386
+
387
+ // ═════════════════════════════════════════════════════════════════════════
388
+ // rt-host-a LOOP #4 — coordinator injects the RAW message content, but the worker only builds a
389
+ // result_envelope when it sees the RENDERED protocol block ending [team-agent-token:<message_id>]. So
390
+ // workers go WORKING but never report -> results=0, no round-trip. delivery.rs:131 injects
391
+ // InjectPayload::Text(message.content) (bare). The fix (leader): port render_message before inject —
392
+ // GOLDEN (rust_core.py:60-73, captured live):
393
+ // "Team Agent message from {sender}[ for {task_id}]:\n\n{content}\n\n[team-agent-token:{message_id}]"
394
+ // ═════════════════════════════════════════════════════════════════════════
395
+
396
+ // RED — deliver must inject the RENDERED protocol block (header + content + [team-agent-token:<id>]),
397
+ // NOT the raw content. Today the payload is the bare "do the thing" (no header, no token) -> RED.
398
+ #[test]
399
+ fn spine_delivery_injects_rendered_protocol_block_not_raw_content() {
400
+ let ws = tmp_ws("deliver-render");
401
+ let store = store_for(&ws);
402
+ let log = EventLog::new(&ws);
403
+ let mid = store.create_message(Some("t1"), "leader", "w1", "do the thing", None, true, None).unwrap();
404
+ let state = serde_json::json!({
405
+ "session_name": "team-x",
406
+ "agents": {"w1": {"status": "idle", "window": "w1"}}
407
+ });
408
+ let transport = OfflineTransport::new();
409
+
410
+ let _ = deliver_pending_messages(&ws, &state, &transport, &log).unwrap();
411
+
412
+ let recorded = transport.inject_payloads();
413
+ assert_eq!(recorded.len(), 1, "deliver must inject the one pending message exactly once; got {recorded:?}");
414
+ let payload = recorded[0].clone();
415
+ // THE BUG (rt-host-a #4): deliver injects the RAW content -> the worker never reports (it only builds
416
+ // a result_envelope on the rendered block with the token).
417
+ assert_ne!(
418
+ payload, "do the thing",
419
+ "deliver must NOT inject the bare content; the worker only reports on the rendered protocol block \
420
+ (with [team-agent-token:<id>]) — bare text -> WORKING but never reports -> results=0; got {payload:?}"
421
+ );
422
+ assert!(
423
+ payload.contains("Team Agent message from leader for t1:"),
424
+ "the injected payload must be the rendered protocol header (sender + task): 'Team Agent message \
425
+ from leader for t1:'; got {payload:?}"
426
+ );
427
+ assert!(
428
+ payload.contains("do the thing"),
429
+ "the rendered block must carry the content; got {payload:?}"
430
+ );
431
+ let token_line = format!("[team-agent-token:{mid}]");
432
+ assert!(
433
+ payload.contains(&token_line),
434
+ "the rendered block must end with the token line {token_line:?} (token == message_id) so the \
435
+ worker builds a result_envelope; got {payload:?}"
436
+ );
437
+ }
@@ -0,0 +1,192 @@
1
+ //! leader_panes.py — trust 自动应答 (step 10 主拥,step 11 借;card §22)。§11 canonical-token
2
+ //! trust matcher:**只对自己工作目录 realpath 全等**才应答,禁 basename/startswith/子串/反推 cwd。
3
+
4
+ use std::path::Path;
5
+
6
+ use crate::event_log::EventLog;
7
+ use crate::transport::{InjectPayload, Key, PaneId, Target, Transport};
8
+
9
+ use super::{MessagingError, PaneWidthQuery};
10
+
11
+ /// trust 自动应答结果 (`leader_panes.py:383` `attempt_trust_auto_answer` 返回 dict 的 typed 版)。
12
+ #[derive(Debug, Clone, PartialEq, Eq)]
13
+ pub struct TrustAnswerOutcome {
14
+ pub ok: bool,
15
+ pub answered: bool,
16
+ /// `not_opted_in`/`pane_id_missing`/`already_answered`/`workspace_dir_mismatch`/
17
+ /// `trust_auto_answered`/`tmux_send_keys_failed`。
18
+ pub reason: String,
19
+ pub action: Option<String>,
20
+ }
21
+
22
+ /// `attempt_trust_auto_answer` (`leader_panes.py:383`):**只对自己工作目录 realpath 全等**才
23
+ /// 自动应答 Codex trust (card §124:禁 basename/startswith/子串/反推 cwd)。pane_width 来自
24
+ /// [`PaneWidthQuery`] 注入 (`state["pane_width"]`),失败时 matcher 退回精确相等。
25
+ pub fn attempt_trust_auto_answer(
26
+ workspace: &Path,
27
+ transport: &dyn Transport,
28
+ pane_id: Option<&PaneId>,
29
+ pane_capture_tail: &str,
30
+ pane_width: &PaneWidthQuery,
31
+ event_log: &EventLog,
32
+ ) -> Result<TrustAnswerOutcome, MessagingError> {
33
+ let Some(pane_id) = pane_id else {
34
+ return Ok(TrustAnswerOutcome {
35
+ ok: false,
36
+ answered: false,
37
+ reason: "pane_id_missing".to_string(),
38
+ action: None,
39
+ });
40
+ };
41
+ let canonical = match std::fs::canonicalize(workspace) {
42
+ Ok(path) => path,
43
+ Err(err) => {
44
+ return Ok(TrustAnswerOutcome {
45
+ ok: false,
46
+ answered: false,
47
+ reason: format!("workspace_realpath_failed:{err}"),
48
+ action: Some("prompt_leader".to_string()),
49
+ });
50
+ }
51
+ };
52
+ if workspace_matches_prompt_tail(&canonical, pane_capture_tail, pane_width) {
53
+ let target = Target::Pane(pane_id.clone());
54
+ match submit_trust_answer(transport, &target) {
55
+ Ok(true) => {
56
+ event_log.write(
57
+ "leader_panes.trust_auto_answered",
58
+ serde_json::json!({"pane_id": pane_id.as_str(), "workspace": canonical.to_string_lossy()}),
59
+ )?;
60
+ }
61
+ Ok(false) => {
62
+ event_log.write(
63
+ "leader_panes.trust_auto_answer_transport_panic",
64
+ serde_json::json!({"pane_id": pane_id.as_str()}),
65
+ )?;
66
+ }
67
+ Err(error) => {
68
+ event_log.write(
69
+ "leader_panes.trust_auto_answer_failed",
70
+ serde_json::json!({"pane_id": pane_id.as_str(), "error": error.to_string()}),
71
+ )?;
72
+ return Ok(TrustAnswerOutcome {
73
+ ok: false,
74
+ answered: false,
75
+ reason: "tmux_send_keys_failed".to_string(),
76
+ action: Some("prompt_leader".to_string()),
77
+ });
78
+ }
79
+ }
80
+ return Ok(TrustAnswerOutcome {
81
+ ok: true,
82
+ answered: true,
83
+ reason: "trust_auto_answered".to_string(),
84
+ action: None,
85
+ });
86
+ }
87
+ Ok(TrustAnswerOutcome {
88
+ ok: false,
89
+ answered: false,
90
+ reason: "workspace_dir_mismatch".to_string(),
91
+ action: Some("prompt_leader".to_string()),
92
+ })
93
+ }
94
+
95
+ fn submit_trust_answer(
96
+ transport: &dyn Transport,
97
+ target: &Target,
98
+ ) -> Result<bool, crate::transport::TransportError> {
99
+ let submitted = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
100
+ transport.inject(target, &InjectPayload::Empty, Key::Enter, false)
101
+ }));
102
+ match submitted {
103
+ Ok(result) => result.map(|_| true),
104
+ Err(_) => Ok(false),
105
+ }
106
+ }
107
+
108
+ fn workspace_matches_prompt_tail(
109
+ canonical_workspace: &Path,
110
+ pane_capture_tail: &str,
111
+ pane_width: &PaneWidthQuery,
112
+ ) -> bool {
113
+ let workspace_text = canonical_workspace.to_string_lossy();
114
+ for token in path_tokens(pane_capture_tail) {
115
+ if token_matches_workspace(&token, &workspace_text, pane_capture_tail, pane_width) {
116
+ return true;
117
+ }
118
+ }
119
+ false
120
+ }
121
+
122
+ fn path_tokens(text: &str) -> Vec<String> {
123
+ text.split_whitespace()
124
+ .filter_map(|part| {
125
+ let token = part.trim_matches(token_trim_char);
126
+ if token.contains('/') {
127
+ Some(token.to_string())
128
+ } else {
129
+ None
130
+ }
131
+ })
132
+ .collect()
133
+ }
134
+
135
+ fn token_trim_char(c: char) -> bool {
136
+ matches!(
137
+ c,
138
+ '?' | '!' | ',' | ';' | ':' | '"' | '\'' | '`' | '[' | ']' | '(' | ')' | '{' | '}'
139
+ | '<' | '>' | '│' | '┃' | '┆' | '┊' | '┌' | '┐' | '└' | '┘' | '─' | '═'
140
+ )
141
+ }
142
+
143
+ fn token_matches_workspace(
144
+ token: &str,
145
+ workspace_text: &str,
146
+ pane_capture_tail: &str,
147
+ pane_width: &PaneWidthQuery,
148
+ ) -> bool {
149
+ if let Ok(path) = std::fs::canonicalize(token) {
150
+ return path.to_string_lossy() == workspace_text;
151
+ }
152
+ if token.contains('…') || token.contains("...") {
153
+ return ellipsis_matches_workspace(token, workspace_text);
154
+ }
155
+ match pane_width {
156
+ PaneWidthQuery::Ok { pane_width } => {
157
+ workspace_text.starts_with(token)
158
+ && token.len() < workspace_text.len()
159
+ && token_reaches_right_edge(token, pane_capture_tail, *pane_width)
160
+ }
161
+ PaneWidthQuery::Failed { .. } => false,
162
+ }
163
+ }
164
+
165
+ fn ellipsis_matches_workspace(token: &str, workspace_text: &str) -> bool {
166
+ let parts: Vec<&str> = if token.contains('…') {
167
+ token.split('…').collect()
168
+ } else {
169
+ token.split("...").collect()
170
+ };
171
+ if parts.len() != 2 {
172
+ return false;
173
+ }
174
+ let head = parts[0];
175
+ let tail = parts[1];
176
+ !head.is_empty()
177
+ && !tail.is_empty()
178
+ && workspace_text.starts_with(head)
179
+ && workspace_text.ends_with(tail)
180
+ }
181
+
182
+ fn token_reaches_right_edge(token: &str, text: &str, pane_width: u32) -> bool {
183
+ let width = match usize::try_from(pane_width) {
184
+ Ok(width) if width > 0 => width,
185
+ _ => return false,
186
+ };
187
+ text.lines().any(|line| {
188
+ line.find(token)
189
+ .map(|idx| idx.saturating_add(token.len()) >= width)
190
+ .unwrap_or(false)
191
+ })
192
+ }