@team-agent/installer 0.2.11 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (326) hide show
  1. package/Cargo.lock +744 -0
  2. package/Cargo.toml +34 -0
  3. package/crates/team-agent/Cargo.toml +33 -0
  4. package/crates/team-agent/src/cli/adapters.rs +1343 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +554 -0
  6. package/crates/team-agent/src/cli/emit.rs +1204 -0
  7. package/crates/team-agent/src/cli/helpers.rs +88 -0
  8. package/crates/team-agent/src/cli/leader.rs +216 -0
  9. package/crates/team-agent/src/cli/mod.rs +1207 -0
  10. package/crates/team-agent/src/cli/profile.rs +306 -0
  11. package/crates/team-agent/src/cli/send.rs +215 -0
  12. package/crates/team-agent/src/cli/status.rs +179 -0
  13. package/crates/team-agent/src/cli/status_port.rs +502 -0
  14. package/crates/team-agent/src/cli/tests/base.rs +616 -0
  15. package/crates/team-agent/src/cli/tests/compile.rs +96 -0
  16. package/crates/team-agent/src/cli/tests/divergence.rs +509 -0
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +333 -0
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +395 -0
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +675 -0
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +390 -0
  21. package/crates/team-agent/src/cli/tests/mod.rs +97 -0
  22. package/crates/team-agent/src/cli/tests/peer_allow.rs +137 -0
  23. package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +302 -0
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +305 -0
  25. package/crates/team-agent/src/cli/tests/status_send.rs +385 -0
  26. package/crates/team-agent/src/cli/tests/verb_profile.rs +182 -0
  27. package/crates/team-agent/src/cli/tests/verb_settle.rs +236 -0
  28. package/crates/team-agent/src/cli/tests/verb_validate.rs +184 -0
  29. package/crates/team-agent/src/cli/types.rs +605 -0
  30. package/crates/team-agent/src/compiler/tests.rs +701 -0
  31. package/crates/team-agent/src/compiler.rs +489 -0
  32. package/crates/team-agent/src/coordinator/backoff.rs +153 -0
  33. package/crates/team-agent/src/coordinator/health.rs +557 -0
  34. package/crates/team-agent/src/coordinator/mod.rs +80 -0
  35. package/crates/team-agent/src/coordinator/orphan.rs +179 -0
  36. package/crates/team-agent/src/coordinator/tests/abnormal.rs +255 -0
  37. package/crates/team-agent/src/coordinator/tests/basics.rs +262 -0
  38. package/crates/team-agent/src/coordinator/tests/daemon.rs +323 -0
  39. package/crates/team-agent/src/coordinator/tests/health_sync.rs +263 -0
  40. package/crates/team-agent/src/coordinator/tests/main_preserved.rs +136 -0
  41. package/crates/team-agent/src/coordinator/tests/mod.rs +310 -0
  42. package/crates/team-agent/src/coordinator/tests/spine.rs +261 -0
  43. package/crates/team-agent/src/coordinator/tests/takeover.rs +227 -0
  44. package/crates/team-agent/src/coordinator/tests/tick_core.rs +256 -0
  45. package/crates/team-agent/src/coordinator/tests/watch.rs +167 -0
  46. package/crates/team-agent/src/coordinator/tick.rs +2032 -0
  47. package/crates/team-agent/src/coordinator/types.rs +584 -0
  48. package/crates/team-agent/src/db/migration.rs +716 -0
  49. package/crates/team-agent/src/db/mod.rs +23 -0
  50. package/crates/team-agent/src/db/schema.rs +378 -0
  51. package/crates/team-agent/src/event_log.rs +375 -0
  52. package/crates/team-agent/src/fake_worker.rs +253 -0
  53. package/crates/team-agent/src/leader/helpers.rs +190 -0
  54. package/crates/team-agent/src/leader/inject.rs +33 -0
  55. package/crates/team-agent/src/leader/lease.rs +1084 -0
  56. package/crates/team-agent/src/leader/mod.rs +99 -0
  57. package/crates/team-agent/src/leader/owner_bind.rs +292 -0
  58. package/crates/team-agent/src/leader/rediscover/tests.rs +526 -0
  59. package/crates/team-agent/src/leader/rediscover.rs +1101 -0
  60. package/crates/team-agent/src/leader/start.rs +273 -0
  61. package/crates/team-agent/src/leader/takeover.rs +235 -0
  62. package/crates/team-agent/src/leader/tests/basics.rs +183 -0
  63. package/crates/team-agent/src/leader/tests/byte_findings.rs +237 -0
  64. package/crates/team-agent/src/leader/tests/identity.rs +206 -0
  65. package/crates/team-agent/src/leader/tests/idle.rs +272 -0
  66. package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
  67. package/crates/team-agent/src/leader/tests/lease_claim.rs +410 -0
  68. package/crates/team-agent/src/leader/tests/mod.rs +125 -0
  69. package/crates/team-agent/src/leader/tests/rediscover.rs +351 -0
  70. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +204 -0
  71. package/crates/team-agent/src/leader/types.rs +489 -0
  72. package/crates/team-agent/src/lib.rs +85 -0
  73. package/crates/team-agent/src/lifecycle/display.rs +228 -0
  74. package/crates/team-agent/src/lifecycle/helpers.rs +112 -0
  75. package/crates/team-agent/src/lifecycle/launch/plan.rs +227 -0
  76. package/crates/team-agent/src/lifecycle/launch.rs +2109 -0
  77. package/crates/team-agent/src/lifecycle/mod.rs +62 -0
  78. package/crates/team-agent/src/lifecycle/restart/agent.rs +533 -0
  79. package/crates/team-agent/src/lifecycle/restart/common.rs +517 -0
  80. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +41 -0
  81. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +268 -0
  82. package/crates/team-agent/src/lifecycle/restart/remove.rs +780 -0
  83. package/crates/team-agent/src/lifecycle/restart/selection.rs +208 -0
  84. package/crates/team-agent/src/lifecycle/restart/team_state.rs +242 -0
  85. package/crates/team-agent/src/lifecycle/restart.rs +76 -0
  86. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +455 -0
  87. package/crates/team-agent/src/lifecycle/tests/core.rs +989 -0
  88. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +583 -0
  89. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +985 -0
  90. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +265 -0
  91. package/crates/team-agent/src/lifecycle/tests.rs +27 -0
  92. package/crates/team-agent/src/lifecycle/types.rs +710 -0
  93. package/crates/team-agent/src/main.rs +41 -0
  94. package/crates/team-agent/src/mcp_server/helpers.rs +228 -0
  95. package/crates/team-agent/src/mcp_server/mod.rs +183 -0
  96. package/crates/team-agent/src/mcp_server/normalize.rs +312 -0
  97. package/crates/team-agent/src/mcp_server/tests/golden.rs +283 -0
  98. package/crates/team-agent/src/mcp_server/tests/normalize.rs +244 -0
  99. package/crates/team-agent/src/mcp_server/tests/scoped.rs +189 -0
  100. package/crates/team-agent/src/mcp_server/tests/send.rs +222 -0
  101. package/crates/team-agent/src/mcp_server/tests/tools.rs +158 -0
  102. package/crates/team-agent/src/mcp_server/tests/wire.rs +187 -0
  103. package/crates/team-agent/src/mcp_server/tests.rs +38 -0
  104. package/crates/team-agent/src/mcp_server/tools.rs +603 -0
  105. package/crates/team-agent/src/mcp_server/types.rs +421 -0
  106. package/crates/team-agent/src/mcp_server/wire.rs +468 -0
  107. package/crates/team-agent/src/message_store.rs +767 -0
  108. package/crates/team-agent/src/messaging/activity.rs +433 -0
  109. package/crates/team-agent/src/messaging/delivery.rs +743 -0
  110. package/crates/team-agent/src/messaging/helpers.rs +209 -0
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +329 -0
  112. package/crates/team-agent/src/messaging/mod.rs +147 -0
  113. package/crates/team-agent/src/messaging/peers.rs +32 -0
  114. package/crates/team-agent/src/messaging/results.rs +553 -0
  115. package/crates/team-agent/src/messaging/scheduler.rs +344 -0
  116. package/crates/team-agent/src/messaging/selftest.rs +100 -0
  117. package/crates/team-agent/src/messaging/send.rs +578 -0
  118. package/crates/team-agent/src/messaging/tests/basic.rs +357 -0
  119. package/crates/team-agent/src/messaging/tests/main_preserved.rs +122 -0
  120. package/crates/team-agent/src/messaging/tests/mod.rs +293 -0
  121. package/crates/team-agent/src/messaging/tests/runtime.rs +1422 -0
  122. package/crates/team-agent/src/messaging/tests/spine.rs +437 -0
  123. package/crates/team-agent/src/messaging/trust.rs +192 -0
  124. package/crates/team-agent/src/messaging/types.rs +355 -0
  125. package/crates/team-agent/src/messaging/watchers.rs +591 -0
  126. package/crates/team-agent/src/model/enums.rs +311 -0
  127. package/crates/team-agent/src/model/errors.rs +17 -0
  128. package/crates/team-agent/src/model/ids.rs +155 -0
  129. package/crates/team-agent/src/model/mod.rs +22 -0
  130. package/crates/team-agent/src/model/paths.rs +228 -0
  131. package/crates/team-agent/src/model/permissions.rs +567 -0
  132. package/crates/team-agent/src/model/routing.rs +340 -0
  133. package/crates/team-agent/src/model/spec.rs +680 -0
  134. package/crates/team-agent/src/model/task_graph.rs +380 -0
  135. package/crates/team-agent/src/model/testdata/fuzz.golden.yaml +43 -0
  136. package/crates/team-agent/src/model/testdata/fuzz.yaml +43 -0
  137. package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +207 -0
  138. package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +206 -0
  139. package/crates/team-agent/src/model/testdata/team.spec.yaml +206 -0
  140. package/crates/team-agent/src/model/yaml/tests.rs +288 -0
  141. package/crates/team-agent/src/model/yaml.rs +800 -0
  142. package/crates/team-agent/src/packaging/install.rs +305 -0
  143. package/crates/team-agent/src/packaging/migrate.rs +30 -0
  144. package/crates/team-agent/src/packaging/mod.rs +82 -0
  145. package/crates/team-agent/src/packaging/repair.rs +24 -0
  146. package/crates/team-agent/src/packaging/tests.rs +829 -0
  147. package/crates/team-agent/src/packaging/types.rs +369 -0
  148. package/crates/team-agent/src/provider/adapter.rs +801 -0
  149. package/crates/team-agent/src/provider/approvals/mod.rs +2 -0
  150. package/crates/team-agent/src/provider/approvals/parsing.rs +452 -0
  151. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +163 -0
  152. package/crates/team-agent/src/provider/classify.rs +456 -0
  153. package/crates/team-agent/src/provider/faults.rs +136 -0
  154. package/crates/team-agent/src/provider/helpers.rs +41 -0
  155. package/crates/team-agent/src/provider/mod.rs +53 -0
  156. package/crates/team-agent/src/provider/startup_prompt.rs +423 -0
  157. package/crates/team-agent/src/provider/tests/adapter.rs +239 -0
  158. package/crates/team-agent/src/provider/tests/classify.rs +240 -0
  159. package/crates/team-agent/src/provider/tests/faults.rs +120 -0
  160. package/crates/team-agent/src/provider/tests/idle.rs +208 -0
  161. package/crates/team-agent/src/provider/tests/wire.rs +213 -0
  162. package/crates/team-agent/src/provider/tests.rs +31 -0
  163. package/crates/team-agent/src/provider/types.rs +424 -0
  164. package/crates/team-agent/src/state/identity.rs +659 -0
  165. package/crates/team-agent/src/state/mod.rs +58 -0
  166. package/crates/team-agent/src/state/owner_gate.rs +423 -0
  167. package/crates/team-agent/src/state/persist.rs +712 -0
  168. package/crates/team-agent/src/state/projection.rs +657 -0
  169. package/crates/team-agent/src/state/selector.rs +105 -0
  170. package/crates/team-agent/src/state/testdata/state-rich.canonical.json +133 -0
  171. package/crates/team-agent/src/tmux_backend/tests.rs +765 -0
  172. package/crates/team-agent/src/tmux_backend.rs +810 -0
  173. package/crates/team-agent/src/transport/test_support.rs +252 -0
  174. package/crates/team-agent/src/transport/tests/behavior.rs +327 -0
  175. package/crates/team-agent/src/transport/tests/mod.rs +199 -0
  176. package/crates/team-agent/src/transport/tests/wire.rs +527 -0
  177. package/crates/team-agent/src/transport.rs +774 -0
  178. package/npm/install.mjs +118 -112
  179. package/package.json +15 -13
  180. package/crates/team-agent-core/Cargo.toml +0 -12
  181. package/crates/team-agent-core/src/lib.rs +0 -332
  182. package/crates/team-agent-core/src/main.rs +0 -152
  183. package/pyproject.toml +0 -18
  184. package/scripts/install.py +0 -88
  185. package/scripts/run_regression_tests.py +0 -83
  186. package/src/team_agent/__init__.py +0 -3
  187. package/src/team_agent/__main__.py +0 -5
  188. package/src/team_agent/_legacy_pane_discovery.py +0 -186
  189. package/src/team_agent/abnormal_track.py +0 -253
  190. package/src/team_agent/approvals/__init__.py +0 -65
  191. package/src/team_agent/approvals/constants.py +0 -6
  192. package/src/team_agent/approvals/parsing.py +0 -176
  193. package/src/team_agent/approvals/runtime_prompts.py +0 -171
  194. package/src/team_agent/approvals/status.py +0 -176
  195. package/src/team_agent/cli/__init__.py +0 -137
  196. package/src/team_agent/cli/commands.py +0 -481
  197. package/src/team_agent/cli/e2e.py +0 -202
  198. package/src/team_agent/cli/helpers.py +0 -226
  199. package/src/team_agent/cli/parser.py +0 -540
  200. package/src/team_agent/compiler.py +0 -334
  201. package/src/team_agent/coordinator/__init__.py +0 -53
  202. package/src/team_agent/coordinator/__main__.py +0 -119
  203. package/src/team_agent/coordinator/lifecycle.py +0 -411
  204. package/src/team_agent/coordinator/metadata.py +0 -61
  205. package/src/team_agent/coordinator/paths.py +0 -17
  206. package/src/team_agent/diagnose/__init__.py +0 -48
  207. package/src/team_agent/diagnose/checks.py +0 -101
  208. package/src/team_agent/diagnose/comms.py +0 -213
  209. package/src/team_agent/diagnose/health.py +0 -241
  210. package/src/team_agent/diagnose/orphan_cleanup.py +0 -364
  211. package/src/team_agent/diagnose/preflight.py +0 -194
  212. package/src/team_agent/diagnose/quick_start.py +0 -324
  213. package/src/team_agent/display/__init__.py +0 -92
  214. package/src/team_agent/display/adaptive.py +0 -511
  215. package/src/team_agent/display/backend.py +0 -46
  216. package/src/team_agent/display/close.py +0 -154
  217. package/src/team_agent/display/ghostty.py +0 -77
  218. package/src/team_agent/display/rebuild.py +0 -102
  219. package/src/team_agent/display/tiling.py +0 -156
  220. package/src/team_agent/display/worker_window.py +0 -114
  221. package/src/team_agent/display/workspace.py +0 -382
  222. package/src/team_agent/errors.py +0 -10
  223. package/src/team_agent/events.py +0 -84
  224. package/src/team_agent/fake_worker.py +0 -80
  225. package/src/team_agent/idle_predicate.py +0 -218
  226. package/src/team_agent/idle_takeover.py +0 -59
  227. package/src/team_agent/idle_takeover_wiring.py +0 -114
  228. package/src/team_agent/launch/__init__.py +0 -41
  229. package/src/team_agent/launch/bootstrap.py +0 -85
  230. package/src/team_agent/launch/config.py +0 -106
  231. package/src/team_agent/launch/core.py +0 -301
  232. package/src/team_agent/launch/requirements.py +0 -57
  233. package/src/team_agent/leader/__init__.py +0 -926
  234. package/src/team_agent/leader_binding.py +0 -183
  235. package/src/team_agent/lifecycle/__init__.py +0 -5
  236. package/src/team_agent/lifecycle/agents.py +0 -278
  237. package/src/team_agent/lifecycle/operations.py +0 -411
  238. package/src/team_agent/lifecycle/paste_buffer_hygiene.py +0 -39
  239. package/src/team_agent/lifecycle/start.py +0 -363
  240. package/src/team_agent/mcp_server/__init__.py +0 -42
  241. package/src/team_agent/mcp_server/__main__.py +0 -7
  242. package/src/team_agent/mcp_server/contracts.py +0 -148
  243. package/src/team_agent/mcp_server/normalize.py +0 -257
  244. package/src/team_agent/mcp_server/server.py +0 -150
  245. package/src/team_agent/mcp_server/tools.py +0 -352
  246. package/src/team_agent/message_store/__init__.py +0 -23
  247. package/src/team_agent/message_store/agent_health.py +0 -113
  248. package/src/team_agent/message_store/core.py +0 -497
  249. package/src/team_agent/message_store/leader_notification_log.py +0 -198
  250. package/src/team_agent/message_store/result_watchers.py +0 -251
  251. package/src/team_agent/message_store/schema.py +0 -308
  252. package/src/team_agent/message_store/schema_migration.py +0 -448
  253. package/src/team_agent/messaging/__init__.py +0 -1
  254. package/src/team_agent/messaging/activity_detector.py +0 -262
  255. package/src/team_agent/messaging/delivery.py +0 -504
  256. package/src/team_agent/messaging/deps.py +0 -247
  257. package/src/team_agent/messaging/idle_alerts.py +0 -423
  258. package/src/team_agent/messaging/internal_delivery.py +0 -46
  259. package/src/team_agent/messaging/leader.py +0 -497
  260. package/src/team_agent/messaging/leader_api_errors.py +0 -216
  261. package/src/team_agent/messaging/leader_panes.py +0 -673
  262. package/src/team_agent/messaging/owner_bypass.py +0 -29
  263. package/src/team_agent/messaging/result_delivery.py +0 -539
  264. package/src/team_agent/messaging/results.py +0 -447
  265. package/src/team_agent/messaging/scheduler.py +0 -450
  266. package/src/team_agent/messaging/send.py +0 -532
  267. package/src/team_agent/messaging/session_drift.py +0 -94
  268. package/src/team_agent/messaging/tmux_io.py +0 -506
  269. package/src/team_agent/messaging/tmux_prompt.py +0 -338
  270. package/src/team_agent/messaging/trust_auto_answer.py +0 -52
  271. package/src/team_agent/orchestrator/__init__.py +0 -376
  272. package/src/team_agent/orchestrator/plan.py +0 -122
  273. package/src/team_agent/orchestrator/state.py +0 -128
  274. package/src/team_agent/paths.py +0 -45
  275. package/src/team_agent/permissions.py +0 -123
  276. package/src/team_agent/profiles/__init__.py +0 -82
  277. package/src/team_agent/profiles/constants.py +0 -19
  278. package/src/team_agent/profiles/core.py +0 -407
  279. package/src/team_agent/profiles/helpers.py +0 -69
  280. package/src/team_agent/profiles/provider_env.py +0 -188
  281. package/src/team_agent/profiles/smoke.py +0 -201
  282. package/src/team_agent/provider_cli/__init__.py +0 -43
  283. package/src/team_agent/provider_cli/adapter.py +0 -172
  284. package/src/team_agent/provider_cli/base.py +0 -48
  285. package/src/team_agent/provider_cli/claude.py +0 -503
  286. package/src/team_agent/provider_cli/codex.py +0 -336
  287. package/src/team_agent/provider_cli/copilot.py +0 -8
  288. package/src/team_agent/provider_cli/fake.py +0 -39
  289. package/src/team_agent/provider_cli/gemini.py +0 -95
  290. package/src/team_agent/provider_cli/opencode.py +0 -8
  291. package/src/team_agent/provider_cli/prompt.py +0 -62
  292. package/src/team_agent/provider_cli/registry.py +0 -18
  293. package/src/team_agent/provider_cli/unsupported.py +0 -32
  294. package/src/team_agent/provider_state/README.md +0 -78
  295. package/src/team_agent/provider_state/__init__.py +0 -91
  296. package/src/team_agent/provider_state/claude.py +0 -86
  297. package/src/team_agent/provider_state/codex.py +0 -84
  298. package/src/team_agent/provider_state/common.py +0 -207
  299. package/src/team_agent/provider_state/registry.py +0 -118
  300. package/src/team_agent/providers.py +0 -163
  301. package/src/team_agent/quality_gates.py +0 -104
  302. package/src/team_agent/restart/__init__.py +0 -34
  303. package/src/team_agent/restart/orchestration.py +0 -554
  304. package/src/team_agent/restart/selection.py +0 -89
  305. package/src/team_agent/restart/snapshot.py +0 -70
  306. package/src/team_agent/routing.py +0 -84
  307. package/src/team_agent/runtime.py +0 -1243
  308. package/src/team_agent/rust_core.py +0 -327
  309. package/src/team_agent/sessions/__init__.py +0 -25
  310. package/src/team_agent/sessions/capture.py +0 -144
  311. package/src/team_agent/sessions/inventory.py +0 -44
  312. package/src/team_agent/sessions/resume.py +0 -135
  313. package/src/team_agent/simple_yaml.py +0 -236
  314. package/src/team_agent/spec.py +0 -370
  315. package/src/team_agent/state.py +0 -693
  316. package/src/team_agent/status/__init__.py +0 -63
  317. package/src/team_agent/status/approvals.py +0 -52
  318. package/src/team_agent/status/compact.py +0 -158
  319. package/src/team_agent/status/constants.py +0 -18
  320. package/src/team_agent/status/inbox.py +0 -58
  321. package/src/team_agent/status/peek.py +0 -117
  322. package/src/team_agent/status/queries.py +0 -199
  323. package/src/team_agent/task_graph.py +0 -80
  324. package/src/team_agent/terminal.py +0 -57
  325. package/src/team_agent/wake.py +0 -58
  326. package/src/team_agent/watch/__init__.py +0 -145
@@ -0,0 +1,213 @@
1
+ #[test]
2
+ fn turnstate_wire_strings_are_exact_python() {
3
+ // provider_state/common.py state values + idle_takeover_contract enum.
4
+ assert_eq!(ser(&TurnState::Idle), "\"idle\"");
5
+ assert_eq!(ser(&TurnState::Working), "\"working\"");
6
+ assert_eq!(ser(&TurnState::IdleInterrupted), "\"idle_interrupted\"");
7
+ assert_eq!(ser(&TurnState::BlockedOnHuman), "\"blocked_on_human\"");
8
+ assert_eq!(ser(&TurnState::Abnormal), "\"abnormal\"");
9
+ assert_eq!(ser(&TurnState::Unknown), "\"unknown\"");
10
+ // round-trip every variant (no rename drift on Deserialize either).
11
+ for (s, v) in [
12
+ ("idle", TurnState::Idle),
13
+ ("working", TurnState::Working),
14
+ ("idle_interrupted", TurnState::IdleInterrupted),
15
+ ("blocked_on_human", TurnState::BlockedOnHuman),
16
+ ("abnormal", TurnState::Abnormal),
17
+ ("unknown", TurnState::Unknown),
18
+ ] {
19
+ let de: TurnState = serde_json::from_str(&format!("\"{s}\"")).expect("de");
20
+ assert_eq!(de, v);
21
+ }
22
+ }
23
+
24
+ #[test]
25
+ fn factkind_wire_strings_are_exact_python() {
26
+ // provider_state/common.py:15-20
27
+ assert_eq!(ser(&FactKind::TurnOpen), "\"turn_open\"");
28
+ assert_eq!(ser(&FactKind::TurnComplete), "\"turn_complete\"");
29
+ assert_eq!(ser(&FactKind::Interrupted), "\"interrupted\"");
30
+ assert_eq!(ser(&FactKind::Failed), "\"failed\"");
31
+ assert_eq!(ser(&FactKind::Approval), "\"approval\"");
32
+ assert_eq!(ser(&FactKind::Error), "\"error\"");
33
+ }
34
+
35
+ #[test]
36
+ fn process_liveness_wire_strings_are_exact_python() {
37
+ // provider_state/common.py:109 three-valued.
38
+ assert_eq!(ser(&ProcessLiveness::Alive), "\"alive\"");
39
+ assert_eq!(ser(&ProcessLiveness::Dead), "\"dead\"");
40
+ assert_eq!(ser(&ProcessLiveness::Unverifiable), "\"unverifiable\"");
41
+ }
42
+
43
+ #[test]
44
+ fn capture_via_wire_strings_are_exact_python() {
45
+ // claude.py:101/371, codex.py:84
46
+ assert_eq!(ser(&CaptureVia::FsWatch), "\"fs_watch\"");
47
+ assert_eq!(ser(&CaptureVia::FsMtimeFallback), "\"fs_mtime_fallback\"");
48
+ assert_eq!(ser(&CaptureVia::FsRepair), "\"fs_repair\"");
49
+ }
50
+
51
+ #[test]
52
+ fn confidence_wire_strings_are_exact_python() {
53
+ assert_eq!(ser(&Confidence::High), "\"high\"");
54
+ assert_eq!(ser(&Confidence::Medium), "\"medium\"");
55
+ assert_eq!(ser(&Confidence::Low), "\"low\"");
56
+ }
57
+
58
+ #[test]
59
+ fn health_status_is_uppercase_python() {
60
+ // approvals/status.py:114-124,98 — these are UPPERCASE in Python.
61
+ assert_eq!(ser(&HealthStatus::Running), "\"RUNNING\"");
62
+ assert_eq!(ser(&HealthStatus::Idle), "\"IDLE\"");
63
+ assert_eq!(ser(&HealthStatus::Working), "\"WORKING\"");
64
+ assert_eq!(ser(&HealthStatus::Blocked), "\"BLOCKED\"");
65
+ assert_eq!(ser(&HealthStatus::Error), "\"ERROR\"");
66
+ assert_eq!(ser(&HealthStatus::Done), "\"DONE\"");
67
+ assert_eq!(ser(&HealthStatus::Stuck), "\"STUCK\"");
68
+ assert_eq!(ser(&HealthStatus::Uncertain), "\"UNCERTAIN\"");
69
+ assert_eq!(ser(&HealthStatus::AwaitingApproval), "\"AWAITING_APPROVAL\"");
70
+ }
71
+
72
+ #[test]
73
+ fn agent_runtime_status_wire_strings_are_exact_python() {
74
+ // approvals/status.py:175 (lowercase snake)
75
+ assert_eq!(ser(&AgentRuntimeStatus::Running), "\"running\"");
76
+ assert_eq!(ser(&AgentRuntimeStatus::Busy), "\"busy\"");
77
+ assert_eq!(ser(&AgentRuntimeStatus::Error), "\"error\"");
78
+ assert_eq!(ser(&AgentRuntimeStatus::Missing), "\"missing\"");
79
+ assert_eq!(ser(&AgentRuntimeStatus::Paused), "\"paused\"");
80
+ assert_eq!(ser(&AgentRuntimeStatus::Stopped), "\"stopped\"");
81
+ assert_eq!(
82
+ ser(&AgentRuntimeStatus::AwaitingTrustPrompt),
83
+ "\"awaiting_trust_prompt\""
84
+ );
85
+ }
86
+
87
+ #[test]
88
+ fn approval_kind_and_auth_hint_wire_strings() {
89
+ // parsing.py:30/55/65
90
+ assert_eq!(ser(&ApprovalKind::McpTool), "\"mcp_tool\"");
91
+ assert_eq!(ser(&ApprovalKind::Command), "\"command\"");
92
+ assert_eq!(ser(&ApprovalKind::Unknown), "\"unknown\"");
93
+ // adapter.py:38
94
+ assert_eq!(ser(&AuthHintStatus::Present), "\"present\"");
95
+ assert_eq!(ser(&AuthHintStatus::Missing), "\"missing\"");
96
+ assert_eq!(
97
+ ser(&AuthHintStatus::MissingOrUnknown),
98
+ "\"missing_or_unknown\""
99
+ );
100
+ assert_eq!(ser(&AuthHintStatus::Unknown), "\"unknown\"");
101
+ }
102
+
103
+ #[test]
104
+ fn no_ping_reason_fixed_strings_match_python() {
105
+ // idle_predicate.py fixed reason strings.
106
+ assert_eq!(
107
+ ser(&NoPingReason::NotArmedNoWorkerTurn),
108
+ "\"not_armed_no_worker_turn\""
109
+ );
110
+ assert_eq!(ser(&NoPingReason::Acknowledged), "\"acknowledged\"");
111
+ assert_eq!(ser(&NoPingReason::DebounceActive), "\"debounce_active\"");
112
+ }
113
+
114
+ #[test]
115
+ fn provider_aliases_share_one_wire_family_and_gemini_cli_renamed() {
116
+ // model::enums::Provider re-export: claude / claude_code are distinct
117
+ // wire keys (providers.py:38-44 ADAPTERS) but reader-side normalize to
118
+ // claude (provider_state/__init__.py:88) — encoded behaviorally below.
119
+ assert_eq!(ser(&Provider::Claude), "\"claude\"");
120
+ assert_eq!(ser(&Provider::ClaudeCode), "\"claude_code\"");
121
+ assert_eq!(ser(&Provider::Codex), "\"codex\"");
122
+ assert_eq!(ser(&Provider::GeminiCli), "\"gemini_cli\"");
123
+ assert_eq!(ser(&Provider::Fake), "\"fake\"");
124
+ // auth_mode wire (profiles/constants.py:6)
125
+ assert_eq!(ser(&AuthMode::CompatibleApi), "\"compatible_api\"");
126
+ assert_eq!(ser(&AuthMode::Subscription), "\"subscription\"");
127
+ assert_eq!(ser(&AuthMode::OfficialApi), "\"official_api\"");
128
+ }
129
+
130
+ // -------------------------------------------------------------------
131
+ // TIER 1 · PREDICATE CONTRACTS (§11 unknown≠idle / _CLOSING) — pure, locked
132
+ // -------------------------------------------------------------------
133
+
134
+ #[test]
135
+ fn only_idle_and_idle_interrupted_allow_takeover_ping() {
136
+ // idle_predicate.py:46-49 — _IDLE_STATES = {idle, idle_interrupted}.
137
+ // C12: interrupted counts as idle (annotated). EVERYTHING else blocks.
138
+ assert!(TurnState::Idle.is_idle_for_takeover());
139
+ assert!(TurnState::IdleInterrupted.is_idle_for_takeover());
140
+ // §11 bug: Unknown must NEVER be idle — explicit, no `_ => idle`.
141
+ assert!(!TurnState::Unknown.is_idle_for_takeover());
142
+ assert!(!TurnState::Working.is_idle_for_takeover());
143
+ assert!(!TurnState::BlockedOnHuman.is_idle_for_takeover());
144
+ assert!(!TurnState::Abnormal.is_idle_for_takeover());
145
+ }
146
+
147
+ #[test]
148
+ fn closing_facts_are_exactly_complete_interrupted_failed() {
149
+ // common.py:22 — _CLOSING = {turn_complete, interrupted, failed}.
150
+ assert!(FactKind::TurnComplete.is_closing());
151
+ assert!(FactKind::Interrupted.is_closing());
152
+ assert!(FactKind::Failed.is_closing());
153
+ // turn_open / approval / error are NOT closing.
154
+ assert!(!FactKind::TurnOpen.is_closing());
155
+ assert!(!FactKind::Approval.is_closing());
156
+ assert!(!FactKind::Error.is_closing());
157
+ }
158
+
159
+ // -------------------------------------------------------------------
160
+ // TIER 1 · NEWTYPE / PAYLOAD shape contracts (bug-085 None穿透) — pure
161
+ // -------------------------------------------------------------------
162
+
163
+ #[test]
164
+ fn newtypes_are_serde_transparent() {
165
+ // §3 id-混传 newtypes must serialize as the bare scalar (transparent).
166
+ assert_eq!(ser(&SessionId::new("abc-123")), "\"abc-123\"");
167
+ assert_eq!(ser(&TurnId::new("t-9")), "\"t-9\"");
168
+ assert_eq!(
169
+ ser(&ApprovalFingerprint::new("700dc5c0a9e4e3e8")),
170
+ "\"700dc5c0a9e4e3e8\""
171
+ );
172
+ // RolloutPath transparent over the path string.
173
+ assert_eq!(
174
+ ser(&RolloutPath::new(PathBuf::from("/x/y.jsonl"))),
175
+ "\"/x/y.jsonl\""
176
+ );
177
+ }
178
+
179
+ #[test]
180
+ fn captured_session_bug085_fallback_shape_roundtrips_with_nulls() {
181
+ // bug-085 (claude.py:365-372): compatible_api fallback yields
182
+ // session_id=None, captured_via=fs_mtime_fallback, confidence=low,
183
+ // rollout_path SET. None must serialize as JSON null (穷尽 Option).
184
+ let cs = CapturedSession {
185
+ session_id: None,
186
+ rollout_path: Some(RolloutPath::new(PathBuf::from("/p/s.jsonl"))),
187
+ captured_via: CaptureVia::FsMtimeFallback,
188
+ attribution_confidence: Confidence::Low,
189
+ spawn_cwd: PathBuf::from("/cwd"),
190
+ };
191
+ let j: serde_json::Value = serde_json::to_value(&cs).expect("to_value");
192
+ assert!(j["session_id"].is_null(), "session_id must be JSON null");
193
+ assert_eq!(j["captured_via"], "fs_mtime_fallback");
194
+ assert_eq!(j["attribution_confidence"], "low");
195
+ assert_eq!(j["rollout_path"], "/p/s.jsonl");
196
+ // round trip back to the same struct.
197
+ let back: CapturedSession = serde_json::from_value(j).expect("from_value");
198
+ assert_eq!(back, cs);
199
+ }
200
+
201
+ // -------------------------------------------------------------------
202
+ // TIER 2 · BEHAVIORAL — drive through get_adapter(..) (unimplemented → RED)
203
+ // Golden semantics annotated; each test panics today at get_adapter and only
204
+ // greens when the adapter + classify/idle/approval pipeline is implemented.
205
+ // -------------------------------------------------------------------
206
+
207
+ // ---- (a) turn-state classify: rollout_path=None / unreadable → Unknown ----
208
+
209
+ // Golden probes (PYTHONPATH=…/src python3 /tmp/probe_classify.py against v0.2.11
210
+ // truth source): every state/reason/turn_id/annotations value below is the
211
+ // exact dict returned by provider_state.read_turn_state.
212
+
213
+ // Fixture builders — minimal JSONL transcripts that the readers recognize.
@@ -0,0 +1,31 @@
1
+ // ===========================================================================
2
+ // RED CONTRACTS (wave-2) — step 8 provider parity, encoded against v0.2.11
3
+ // truth source `team-agent-public` @ 439bef8. Golden values captured via
4
+ // PYTHONPATH=…/src python3 /tmp/probe_{turn_state,idle_predicate,approvals}.py.
5
+ //
6
+ // Two tiers:
7
+ // * WIRE/PREDICATE tier — pure enum serde + helper-method contracts. These pin
8
+ // the exact Python byte strings (`TurnState`/`FactKind`/`Confidence`/…) and
9
+ // the §11 fail-safe predicates so the porter cannot drift a rename or sneak a
10
+ // `_ => idle` fallthrough. (Skeleton enums are already concrete → tier is the
11
+ // locked spec the impl must keep satisfying, not the RED driver.)
12
+ // * BEHAVIORAL tier — drives every parity-critical behavior through
13
+ // `get_adapter(..)` + `ProviderAdapter` trait methods, which are
14
+ // `unimplemented!()` in ROUND-0 → these PANIC = genuinely RED today, and only
15
+ // green once the porter wires the adapters AND the neutral classify/idle/
16
+ // approval pipeline the trait sits on top of.
17
+ // ===========================================================================
18
+ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
19
+ use super::*;
20
+ use serde::Serialize;
21
+ use std::path::PathBuf;
22
+
23
+ fn ser(v: &impl Serialize) -> String {
24
+ serde_json::to_string(v).expect("serialize")
25
+ }
26
+
27
+ include!("tests/wire.rs");
28
+ include!("tests/classify.rs");
29
+ include!("tests/idle.rs");
30
+ include!("tests/adapter.rs");
31
+ include!("tests/faults.rs");
@@ -0,0 +1,424 @@
1
+ //! provider 共享类型:enums / newtypes / ProviderCaps / ProviderError / 捕获+classify payload / 占位结构。
2
+
3
+ use std::path::PathBuf;
4
+
5
+ use serde::{Deserialize, Serialize};
6
+ use thiserror::Error;
7
+
8
+ // ===========================================================================
9
+ // ENUMS (穷尽 + serde rename 到精确 Python 字符串)
10
+ // ===========================================================================
11
+
12
+ /// node 分类结果(`provider_state/common.py` / `idle_takeover.py`)。
13
+ /// **doc §49 铁律:`Unknown` 必须显式 block ping,绝不 fallthrough 成 idle**
14
+ /// (`idle_predicate.py:46-49` 任何非 `{idle, idle_interrupted}` 立刻 block)。
15
+ /// `idle_takeover_contract.md` 列为稳定 contract enum。
16
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
17
+ #[serde(rename_all = "snake_case")]
18
+ pub enum TurnState {
19
+ Idle,
20
+ Working,
21
+ IdleInterrupted,
22
+ BlockedOnHuman,
23
+ Abnormal,
24
+ Unknown,
25
+ }
26
+
27
+ impl TurnState {
28
+ /// take-over predicate 仅对 `{Idle, IdleInterrupted}` 放行 ping
29
+ /// (`idle_predicate.py:46-49`;C12 interrupted 算 idle 带注解)。其余一律 block。
30
+ pub fn is_idle_for_takeover(self) -> bool {
31
+ matches!(self, Self::Idle | Self::IdleInterrupted)
32
+ }
33
+ }
34
+
35
+ /// reader 输出的归一 lifecycle 事件类型(`provider_state/common.py:15-20`)。
36
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
37
+ #[serde(rename_all = "snake_case")]
38
+ pub enum FactKind {
39
+ TurnOpen,
40
+ TurnComplete,
41
+ Interrupted,
42
+ Failed,
43
+ Approval,
44
+ Error,
45
+ }
46
+
47
+ impl FactKind {
48
+ /// `_CLOSING = {complete, interrupted, failed}`(`common.py:22`)——闭合一个 open turn。
49
+ pub fn is_closing(self) -> bool {
50
+ matches!(self, Self::TurnComplete | Self::Interrupted | Self::Failed)
51
+ }
52
+ }
53
+
54
+ /// process identity guard 三值判定(`provider_state/common.py:109`)。
55
+ /// **ADJUDICATION**:doc 原名 `Liveness`,与既有 `model::enums::PaneLiveness`
56
+ /// (tmux pane 存活,**不同概念**)同名冲突 → 本 enum 命名 `ProcessLiveness` 避撞。
57
+ /// C4:`Unverifiable ≠ Alive`,绝不乐观读成 working。
58
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
59
+ #[serde(rename_all = "snake_case")]
60
+ pub enum ProcessLiveness {
61
+ Alive,
62
+ Dead,
63
+ Unverifiable,
64
+ }
65
+
66
+ /// agent runtime status(`approvals/status.py:175` / `refresh_agent_runtime_statuses`)。
67
+ /// §19 散字符串态 → enum。归 step 5 state 但 step 8 写。
68
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
69
+ #[serde(rename_all = "snake_case")]
70
+ pub enum AgentRuntimeStatus {
71
+ Running,
72
+ Busy,
73
+ Error,
74
+ Missing,
75
+ Paused,
76
+ Stopped,
77
+ AwaitingTrustPrompt,
78
+ }
79
+
80
+ /// agent health label(`approvals/status.py:114-124,98`)。message-store(step 7)写入。
81
+ /// **注意:Python 此处是大写串**(`"RUNNING"/.../"AWAITING_APPROVAL"`),serde SCREAMING_SNAKE_CASE
82
+ /// (多词变体 `AwaitingApproval` → `"AWAITING_APPROVAL"` 带下划线,非 UPPERCASE 的 "AWAITINGAPPROVAL")。
83
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
84
+ #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
85
+ pub enum HealthStatus {
86
+ Running,
87
+ Idle,
88
+ Working,
89
+ Blocked,
90
+ Error,
91
+ Done,
92
+ Stuck,
93
+ Uncertain,
94
+ AwaitingApproval,
95
+ }
96
+
97
+ pub fn agent_health_status(status: &str) -> HealthStatus {
98
+ match status.to_ascii_lowercase().as_str() {
99
+ "busy" => HealthStatus::Running,
100
+ "running" => HealthStatus::Idle,
101
+ "working" => HealthStatus::Working,
102
+ "paused" | "blocked" | "awaiting_approval" | "awaiting_trust_prompt" => {
103
+ HealthStatus::Blocked
104
+ }
105
+ "error" | "missing" | "interrupted" => HealthStatus::Error,
106
+ "stopped" | "done" => HealthStatus::Done,
107
+ "stuck" => HealthStatus::Stuck,
108
+ "uncertain" => HealthStatus::Uncertain,
109
+ _ => HealthStatus::Idle,
110
+ }
111
+ }
112
+
113
+ /// approval prompt kind(`approvals/parsing.py:30/55/65`)。
114
+ /// 自动 approve 只放行 `McpTool` ∩ 白名单。
115
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
116
+ #[serde(rename_all = "snake_case")]
117
+ pub enum ApprovalKind {
118
+ McpTool,
119
+ Command,
120
+ Unknown,
121
+ }
122
+
123
+ /// session 捕获来源(`claude.py:101/371`、`codex.py:84`)。golden fixture 字段,
124
+ /// §5 EventLog 名稳定一致。
125
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
126
+ #[serde(rename_all = "snake_case")]
127
+ pub enum CaptureVia {
128
+ FsWatch,
129
+ FsMtimeFallback,
130
+ FsRepair,
131
+ }
132
+
133
+ /// attribution confidence(doc §56)。bug-085 fallback 固定 `Low`。
134
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
135
+ #[serde(rename_all = "snake_case")]
136
+ pub enum Confidence {
137
+ High,
138
+ Medium,
139
+ Low,
140
+ }
141
+
142
+ /// auth_hint 状态(`adapter.py:38` 等)。doctor 用。
143
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
144
+ #[serde(rename_all = "snake_case")]
145
+ pub enum AuthHintStatus {
146
+ Present,
147
+ Missing,
148
+ MissingOrUnknown,
149
+ Unknown,
150
+ }
151
+
152
+ /// take-over reason / event 名(`idle_predicate.py` `_result` 的 `reason` 字段)。
153
+ /// §5 EventLog:JSON 事件名 Rust 必须字节一致。固定串变体用 serde rename;
154
+ /// `Node(TurnState)` 承载 `"node_<state>"` 动态形态(`idle_predicate.py:49`)。
155
+ ///
156
+ /// **铁律(BLOOD-LINE PIN, bug-071/077/085)**:任何非 `{Idle, IdleInterrupted}` node
157
+ /// 必须命中 `Node(state)` 并 block ping;`Unknown` 渲染 `"node_unknown"`,绝不 fallthrough idle。
158
+ /// `reason_str()` 给出 Python `read_turn_state`/`evaluate_takeover_reminder` 的精确 reason 串。
159
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
160
+ #[serde(rename_all = "snake_case")]
161
+ pub enum NoPingReason {
162
+ /// worker 从未 open 过 turn(`idle_predicate.py:61`)。
163
+ NotArmedNoWorkerTurn,
164
+ /// suppress flag 置位(acknowledge-idle 后,`idle_predicate.py:63`)。
165
+ Acknowledged,
166
+ /// armed 但 elapsed < debounce(`idle_predicate.py:65`)。
167
+ DebounceActive,
168
+ /// 本 episode 已 ping 过一次(`idle_predicate.py:67`)。
169
+ AlreadyPingedThisEpisode,
170
+ /// 全 idle + armed + debounce 到期 → **ping 侧 reason**(`idle_predicate.py:72`,should_ping=True)。
171
+ AllIdleDebounceElapsed,
172
+ /// 无 node(`idle_predicate.py:52`)。
173
+ NoNodes,
174
+ /// node 处于非 idle 态被 block,渲染 `"node_<state>"`(`idle_predicate.py:49`)。
175
+ /// `Unknown` → `"node_unknown"`(missing-state 同样归 unknown)。
176
+ Node(TurnState),
177
+ }
178
+
179
+ impl NoPingReason {
180
+ /// Python `_result` 的 `reason` 字段精确串(`idle_predicate.py`)。
181
+ /// 固定变体直接给串;`Node(state)` 拼成 `"node_<state-wire>"`。
182
+ pub fn reason_str(&self) -> String {
183
+ match self {
184
+ Self::NotArmedNoWorkerTurn => "not_armed_no_worker_turn".to_string(),
185
+ Self::Acknowledged => "acknowledged".to_string(),
186
+ Self::DebounceActive => "debounce_active".to_string(),
187
+ Self::AlreadyPingedThisEpisode => "already_pinged_this_episode".to_string(),
188
+ Self::AllIdleDebounceElapsed => "all_idle_debounce_elapsed".to_string(),
189
+ Self::NoNodes => "no_nodes".to_string(),
190
+ Self::Node(state) => {
191
+ let wire = match state {
192
+ TurnState::Idle => "idle",
193
+ TurnState::Working => "working",
194
+ TurnState::IdleInterrupted => "idle_interrupted",
195
+ TurnState::BlockedOnHuman => "blocked_on_human",
196
+ TurnState::Abnormal => "abnormal",
197
+ TurnState::Unknown => "unknown",
198
+ };
199
+ format!("node_{wire}")
200
+ }
201
+ }
202
+ }
203
+ }
204
+
205
+ // ===========================================================================
206
+ // NEWTYPES (透明 String/PathBuf 包装 — id 混传根因,§3)
207
+ // ===========================================================================
208
+
209
+ /// provider session id(uuid)。**bug-085:`None` 合法**(compatible_api fallback,
210
+ /// `claude.py:366`)——调用面用 `Option<SessionId>`,穷尽 match `None` 不崩。
211
+ #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
212
+ #[serde(transparent)]
213
+ pub struct SessionId(pub String);
214
+
215
+ impl SessionId {
216
+ pub fn new(s: impl Into<String>) -> Self {
217
+ Self(s.into())
218
+ }
219
+ pub fn as_str(&self) -> &str {
220
+ &self.0
221
+ }
222
+ }
223
+
224
+ impl std::fmt::Display for SessionId {
225
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226
+ f.write_str(&self.0)
227
+ }
228
+ }
229
+
230
+ /// provider session 日志路径(claude transcript / codex rollout)。
231
+ /// **bug-085:`None` → node 留 `Unknown`,不得当 idle**(doc §53)。
232
+ #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
233
+ #[serde(transparent)]
234
+ pub struct RolloutPath(pub PathBuf);
235
+
236
+ impl RolloutPath {
237
+ pub fn new(p: impl Into<PathBuf>) -> Self {
238
+ Self(p.into())
239
+ }
240
+ pub fn as_path(&self) -> &std::path::Path {
241
+ &self.0
242
+ }
243
+ }
244
+
245
+ /// turn id(claude `requestId`/`uuid`;codex `turn_id`)。abnormal dedup key 一半
246
+ /// `(signature, turn_id)`(C8,`claude.py:54-62`)。`None` 合法 → `Option<TurnId>`。
247
+ #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
248
+ #[serde(transparent)]
249
+ pub struct TurnId(pub String);
250
+
251
+ impl TurnId {
252
+ pub fn new(s: impl Into<String>) -> Self {
253
+ Self(s.into())
254
+ }
255
+ pub fn as_str(&self) -> &str {
256
+ &self.0
257
+ }
258
+ }
259
+
260
+ /// approval prompt fingerprint = sha256[:16] hex(`approvals/parsing.py:138`)。
261
+ /// 幂等 dedup key。doc §62 给两选项(`[u8;8]` / `String`)——选 transparent `String`
262
+ /// 与既有 hex newtype 风格(`LeaderSessionUuid`)一致,且序列化字节 == Python hex 串。
263
+ #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
264
+ #[serde(transparent)]
265
+ pub struct ApprovalFingerprint(pub String);
266
+
267
+ impl ApprovalFingerprint {
268
+ pub fn new(s: impl Into<String>) -> Self {
269
+ Self(s.into())
270
+ }
271
+ pub fn as_str(&self) -> &str {
272
+ &self.0
273
+ }
274
+ }
275
+
276
+ /// abnormal/fault fact 签名(`claude.py:51/57` `"signature"`、`codex.py:65/80`)。
277
+ /// C8 dedup key 的另一半 `(Signature, Option<TurnId>)`。固定取值:
278
+ /// `api_error` / `tool_result_is_error` / `turn_failed` / `approval_required`。
279
+ /// transparent String 与 hex newtype 风格一致,序列化字节 == Python 串。
280
+ #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
281
+ #[serde(transparent)]
282
+ pub struct Signature(pub String);
283
+
284
+ impl Signature {
285
+ pub fn new(s: impl Into<String>) -> Self {
286
+ Self(s.into())
287
+ }
288
+ pub fn as_str(&self) -> &str {
289
+ &self.0
290
+ }
291
+ }
292
+
293
+ // ===========================================================================
294
+ // STRUCT
295
+ // ===========================================================================
296
+
297
+ /// provider 能力位(doc §59)。`supports_session_fork` 还依赖
298
+ /// `auth_mode != compatible_api`(`claude.py:54`/`codex.py:45`)——`fork` 的运行期
299
+ /// 真值由 `ProviderAdapter::fork` + auth_mode 共同决定,此 struct 是静态声明。
300
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
301
+ pub struct ProviderCaps {
302
+ pub resume: bool,
303
+ pub fork: bool,
304
+ pub native_mcp_config: bool,
305
+ pub writes_global_settings: bool,
306
+ }
307
+
308
+ // ===========================================================================
309
+ // ERROR
310
+ // ===========================================================================
311
+
312
+ /// step 8 provider 错误。**ADJUDICATION**:不复用 `model::errors::ModelError`
313
+ /// (那是 spec/envelope 校验层);provider 层有自己的失败语义:
314
+ /// - `CapabilityUnsupported`:占位 provider(copilot/opencode)调用即拒
315
+ /// (`unsupported.py:31` `ProviderCapabilityError`)——doc §126「绝不静默返回空命令」。
316
+ /// - `ResumeUnavailable`:bug-085 / 不可 resume 时**干净 raise 不崩**(`adapter.py` `ResumeUnavailable`)。
317
+ /// - `Io`:tmux capture / send-keys / 文件读写子进程失败(daemon tick 必须返 Result 不 panic)。
318
+ ///
319
+ /// 实现层补充变体时保持 fallible 边界;§10 deny-lock 由 leader 加。
320
+ #[derive(Debug, Error)]
321
+ pub enum ProviderError {
322
+ /// 占位 provider 能力未实现(copilot/opencode)。
323
+ #[error("provider capability unsupported: {0}")]
324
+ CapabilityUnsupported(String),
325
+ /// resume 不可用(bug-085 compatible_api `session_id=None` 等)。
326
+ #[error("resume unavailable: {0}")]
327
+ ResumeUnavailable(String),
328
+ /// session 捕获失败 / 超时。
329
+ #[error("session capture failed: {0}")]
330
+ CaptureFailed(String),
331
+ /// 命令构造 / model 校验失败。
332
+ #[error("provider command error: {0}")]
333
+ Command(String),
334
+ /// tmux / 文件 / 子进程 I/O 失败(返 Result 不 panic,株连 bug-084 是禁区)。
335
+ #[error("provider io error: {0}")]
336
+ Io(String),
337
+ }
338
+
339
+ // ===========================================================================
340
+ // 捕获返回 payload (doc §73:capture_session_id 返 6 键 dict 的 typed 版)
341
+ // ===========================================================================
342
+
343
+ /// `capture_session_id` 成功返回(`claude.py:73`/`codex.py:62` 的 typed dict)。
344
+ /// bug-085:`session_id` 可为 `None`,`rollout_path` 可为 `None`(半状态合法)。
345
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
346
+ pub struct CapturedSession {
347
+ pub session_id: Option<SessionId>,
348
+ pub rollout_path: Option<RolloutPath>,
349
+ pub captured_via: CaptureVia,
350
+ pub attribution_confidence: Confidence,
351
+ pub spawn_cwd: PathBuf,
352
+ }
353
+
354
+ // ===========================================================================
355
+ // CLASSIFY 结果 (provider_state.common._result 的 typed 版 — doc §73)
356
+ // ===========================================================================
357
+
358
+ /// classify 结果来源(`provider_state/common.py:_result` 的 `source` 字段)。
359
+ /// `session_file`:verdict 出自日志 lifecycle fact;`process_guard`:open turn 被
360
+ /// process-identity 判定 demote;`registry`:未知 provider 兜底。
361
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
362
+ #[serde(rename_all = "snake_case")]
363
+ pub enum ClassifySource {
364
+ SessionFile,
365
+ ProcessGuard,
366
+ Registry,
367
+ }
368
+
369
+ /// 中性 classify 结果(`provider_state.common.decide_state`/`_result` 的 6 键 dict)。
370
+ /// **铁律**:`state == Unknown` 时 `reason` 必为 `unreadable_or_empty` /
371
+ /// `no_turn_lifecycle_fact` / `process_identity_unverified` / `unknown_provider`,
372
+ /// 且 `is_idle_for_takeover() == false`(穷尽,无 `_ => idle`)。
373
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
374
+ pub struct ClassifyResult {
375
+ pub state: TurnState,
376
+ pub turn_id: Option<TurnId>,
377
+ pub reason: String,
378
+ pub source: ClassifySource,
379
+ pub annotations: Vec<String>,
380
+ pub diagnostics: Vec<serde_json::Value>,
381
+ }
382
+
383
+ /// abnormal track 消费的 fault/approval fact(`provider_state.read_fault_facts`)。
384
+ /// C8 dedup key = `(signature, turn_id)`。`turn_id` 可 `None`(`api_error` 无 ids)。
385
+ #[derive(Debug, Clone, PartialEq, Eq)]
386
+ pub struct FaultFact {
387
+ pub signature: Signature,
388
+ pub turn_id: Option<TurnId>,
389
+ pub kind: FactKind,
390
+ }
391
+
392
+ /// take-over reminder 判定结果(`idle_predicate.evaluate_takeover_reminder` `_result`)。
393
+ /// `interrupted_nodes`:C12 idle_interrupted node 的 id 列表(注解穿透进 ping 结果)。
394
+ /// `message`:should_ping 时携带的中性提醒文案,否则 `None`。
395
+ #[derive(Debug, Clone, PartialEq, Eq)]
396
+ pub struct RemindResult {
397
+ pub should_ping: bool,
398
+ pub reason: NoPingReason,
399
+ pub interrupted_nodes: Vec<String>,
400
+ pub message: Option<String>,
401
+ }
402
+
403
+ // ===========================================================================
404
+ // 占位结构 (impl 阶段填充;ROUND-0 仅命名让 trait 签名编得过)
405
+ // ===========================================================================
406
+
407
+ /// MCP server 配置(step 6 spec compiler 产出 / 本子系统消费)。
408
+ /// ROUND-0 占位:字段在 step 8 impl 时对照 `ensure_compatible_claude_mcp_config` 填充。
409
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
410
+ pub struct McpConfig {
411
+ /// 占位:实际 server map / transport / env 注入在 impl 时补。
412
+ pub raw: serde_json::Value,
413
+ }
414
+
415
+ /// pane→status 识别正则集(`provider_cli/claude.py:225`/`codex.py:140`
416
+ /// `status_patterns()` 返回的 idle/processing/error 三正则)。
417
+ /// claude:`idle=r"[>❯]\s"` `processing=r"[✶✢✽✻✳·].*…"` `error="Error|Traceback"`;
418
+ /// codex:`idle=r"(›|❯|codex>)"` `processing=r"•.*esc to interrupt"` `error="Error|Traceback|panic"`。
419
+ #[derive(Debug, Clone)]
420
+ pub struct StatusPatterns {
421
+ pub idle: regex::Regex,
422
+ pub processing: regex::Regex,
423
+ pub error: regex::Regex,
424
+ }