@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,375 @@
1
+ //! step 4 · event_log — `events.jsonl` 唯一审计流(真相源 `events.py`)。
2
+ //!
3
+ //! 每行一个事件:`{"ts": iso8601_utc, "event": type, ...fields}`,经
4
+ //! `json.dumps(sort_keys=True, ensure_ascii=False)` 序列化。**字节对拍要点(对抗检查)**:
5
+ //! - Python `json.dumps` 默认分隔符是 `", "` / `": "`(**带空格**)→ 自定义 [`PythonFormatter`]
6
+ //! (serde_json 默认无空格)。
7
+ //! - `sort_keys=True` 是**递归**排序 → [`sort_value`](preserve_order 下按 sorted 序重建;
8
+ //! envelope/state 仍需插入序,故不全局改 serde_json)。
9
+ //! - `ensure_ascii=False` → serde_json 默认即不转义非 ASCII(UTF-8 字面)。
10
+ //! - 轮转:5 MiB × 保留 5 archives(`events.jsonl.1..5`)。
11
+ //!
12
+ //! §10:无 unwrap/expect/panic;rotation 的 rename 失败best-effort 忽略(对应 Python
13
+ //! `except OSError: pass`,bug-084 的 os.replace 崩溃教训)。
14
+ //!
15
+ //! **有意分歧/已知边界(对抗检查确认,事件流里不会触发或不复刻 Python bug)**:
16
+ //! - `tail` 用 `\n` 行分割(JSONL 正解),**不复刻** Python `splitlines()` 在 U+2028/U+2029/
17
+ //! NEL 上误切 JSON 行的潜在 bug(§11 不重蹈)。
18
+ //! - 事件字段**可含 float**(如 `waited_sec: 0.0`,见 bug_082 golden):正常小数 serde_json 与
19
+ //! Python 字节一致(`0.0`/`0.5`…),由真 fixture round-trip 锁死。**已知边界**:`|v|<1e-4` 的
20
+ //! 指数浮点(`1e-07` vs serde `1e-7`、`1e-05` vs `0.00001`)与 NaN/Infinity、超 i64 整数会漂移 ——
21
+ //! 实测事件流不出现这些值,故未启 serde_json `arbitrary_precision`(全局影响 Number/preserve_order)
22
+ //! 或自定义 CPython float_repr;若将来引入,需重审。
23
+ #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
24
+
25
+ use std::io::Write as _;
26
+ use std::path::{Path, PathBuf};
27
+
28
+ use serde::Serialize as _;
29
+ use serde_json::Value;
30
+ use thiserror::Error;
31
+
32
+ use crate::model::paths::logs_dir;
33
+
34
+ /// `events.py:17`:5 MiB。
35
+ pub const EVENT_LOG_ROTATE_BYTES: u64 = 5 * 1024 * 1024;
36
+ /// `events.py:18`:保留 5 个 archive。
37
+ pub const EVENT_LOG_ARCHIVE_KEEP: u32 = 5;
38
+
39
+ #[derive(Debug, Error)]
40
+ pub enum EventLogError {
41
+ #[error("io: {0}")]
42
+ Io(#[from] std::io::Error),
43
+ #[error("json: {0}")]
44
+ Json(#[from] serde_json::Error),
45
+ }
46
+
47
+ /// serde_json `Formatter`,复刻 Python `json.dumps` 默认分隔符 `", "` / `": "`。
48
+ struct PythonFormatter;
49
+
50
+ impl serde_json::ser::Formatter for PythonFormatter {
51
+ fn begin_array_value<W: ?Sized + std::io::Write>(&mut self, w: &mut W, first: bool) -> std::io::Result<()> {
52
+ if first {
53
+ Ok(())
54
+ } else {
55
+ w.write_all(b", ")
56
+ }
57
+ }
58
+ fn begin_object_key<W: ?Sized + std::io::Write>(&mut self, w: &mut W, first: bool) -> std::io::Result<()> {
59
+ if first {
60
+ Ok(())
61
+ } else {
62
+ w.write_all(b", ")
63
+ }
64
+ }
65
+ fn begin_object_value<W: ?Sized + std::io::Write>(&mut self, w: &mut W) -> std::io::Result<()> {
66
+ w.write_all(b": ")
67
+ }
68
+ }
69
+
70
+ /// 递归把 object 键排序(`sort_keys=True`)。preserve_order 下 `serde_json::Map` 是 IndexMap,
71
+ /// 按 sorted 序插入 → 序列化按插入序 = sorted。
72
+ fn sort_value(v: &Value) -> Value {
73
+ match v {
74
+ Value::Object(m) => {
75
+ let mut keys: Vec<&String> = m.keys().collect();
76
+ keys.sort_unstable();
77
+ let mut out = serde_json::Map::new();
78
+ for k in keys {
79
+ if let Some(val) = m.get(k) {
80
+ out.insert(k.clone(), sort_value(val));
81
+ }
82
+ }
83
+ Value::Object(out)
84
+ }
85
+ Value::Array(a) => Value::Array(a.iter().map(sort_value).collect()),
86
+ other => other.clone(),
87
+ }
88
+ }
89
+
90
+ /// `json.dumps(value, sort_keys=True, ensure_ascii=False)` 的字节等价(传入前先 [`sort_value`])。
91
+ fn to_python_json(value: &Value) -> String {
92
+ let mut buf = Vec::new();
93
+ let mut ser = serde_json::Serializer::with_formatter(&mut buf, PythonFormatter);
94
+ if value.serialize(&mut ser).is_err() {
95
+ return String::new();
96
+ }
97
+ String::from_utf8(buf).unwrap_or_default() // serde_json 必产合法 UTF-8
98
+ }
99
+
100
+ /// `datetime.now(utc).isoformat()` 字节等价:micros==0 省略小数秒(Python isoformat),否则 6 位微秒。
101
+ fn format_ts(dt: chrono::DateTime<chrono::Utc>) -> String {
102
+ if dt.timestamp_subsec_micros() == 0 {
103
+ dt.to_rfc3339_opts(chrono::SecondsFormat::Secs, false)
104
+ } else {
105
+ dt.to_rfc3339_opts(chrono::SecondsFormat::Micros, false)
106
+ }
107
+ }
108
+
109
+ /// `events.py:EventLog`。
110
+ pub struct EventLog {
111
+ path: PathBuf,
112
+ }
113
+
114
+ impl EventLog {
115
+ /// `EventLog(workspace)`:路径 = `<workspace>/.team/logs/events.jsonl`。
116
+ pub fn new(workspace: &Path) -> Self {
117
+ Self { path: logs_dir(workspace).join("events.jsonl") }
118
+ }
119
+
120
+ /// 直接指定 events.jsonl 路径(测试 / 非标准布局)。
121
+ pub fn at(path: PathBuf) -> Self {
122
+ Self { path }
123
+ }
124
+
125
+ /// `EventLog.write`:追加一行 `{ts, event, **fields}`(sort_keys + Python 分隔符)。
126
+ /// 返回合并后的事件 Value。`fields` 应是 object;非 object 视作无字段。
127
+ pub fn write(&self, event_type: &str, fields: Value) -> Result<Value, EventLogError> {
128
+ if let Some(parent) = self.path.parent() {
129
+ std::fs::create_dir_all(parent)?;
130
+ }
131
+ let ts = format_ts(chrono::Utc::now());
132
+ let mut obj = serde_json::Map::new();
133
+ obj.insert("ts".to_string(), Value::String(ts));
134
+ obj.insert("event".to_string(), Value::String(event_type.to_string()));
135
+ if let Value::Object(f) = fields {
136
+ for (k, v) in f {
137
+ obj.insert(k, v);
138
+ }
139
+ }
140
+ let event = Value::Object(obj);
141
+ self.maybe_rotate()?;
142
+ // 单次 write_all(line+"\n"):POSIX O_APPEND 对 <PIPE_BUF 写原子,避免并发写者交错(对抗 P1)。
143
+ let mut bytes = to_python_json(&sort_value(&event)).into_bytes();
144
+ bytes.push(b'\n');
145
+ let mut file = std::fs::OpenOptions::new().create(true).append(true).open(&self.path)?;
146
+ file.write_all(&bytes)?;
147
+ Ok(event)
148
+ }
149
+
150
+ /// `EventLog.tail`:末尾 `limit` 行,逐行 JSON parse;失败行 → `{"raw": line}`。
151
+ pub fn tail(&self, limit: usize) -> Result<Vec<Value>, EventLogError> {
152
+ if !self.path.exists() {
153
+ return Ok(Vec::new());
154
+ }
155
+ let text = std::fs::read_to_string(&self.path)?;
156
+ let lines: Vec<&str> = text.lines().collect();
157
+ // Python lines[-limit:]:limit==0 → lines[0:] = 全部(负零切片怪癖);limit>len → 全部。
158
+ let start = if limit == 0 { 0 } else { lines.len().saturating_sub(limit) };
159
+ let mut out = Vec::new();
160
+ for line in &lines[start..] {
161
+ match serde_json::from_str::<Value>(line) {
162
+ Ok(v) => out.push(v),
163
+ Err(_) => {
164
+ let mut m = serde_json::Map::new();
165
+ m.insert("raw".to_string(), Value::String((*line).to_string()));
166
+ out.push(Value::Object(m));
167
+ }
168
+ }
169
+ }
170
+ Ok(out)
171
+ }
172
+
173
+ /// `events.py:_maybe_rotate`:size >= 5 MiB → 丢最旧 + 移位 + current→.1。rename 失败忽略。
174
+ fn maybe_rotate(&self) -> Result<(), EventLogError> {
175
+ let size = match std::fs::metadata(&self.path) {
176
+ Ok(m) => m.len(),
177
+ Err(_) => return Ok(()), // 文件不存在 → 不轮转(对应 Python FileNotFoundError)
178
+ };
179
+ if size < EVENT_LOG_ROTATE_BYTES {
180
+ return Ok(());
181
+ }
182
+ let oldest = self.archive_path(EVENT_LOG_ARCHIVE_KEEP);
183
+ if oldest.exists() {
184
+ let _ = std::fs::remove_file(&oldest);
185
+ }
186
+ for idx in (1..EVENT_LOG_ARCHIVE_KEEP).rev() {
187
+ let src = self.archive_path(idx);
188
+ if src.exists() {
189
+ let _ = std::fs::rename(&src, self.archive_path(idx + 1));
190
+ }
191
+ }
192
+ let _ = std::fs::rename(&self.path, self.archive_path(1));
193
+ Ok(())
194
+ }
195
+
196
+ fn archive_path(&self, index: u32) -> PathBuf {
197
+ let name = self.path.file_name().map_or_else(String::new, |n| n.to_string_lossy().into_owned());
198
+ self.path.with_file_name(format!("{name}.{index}"))
199
+ }
200
+ }
201
+
202
+ #[cfg(test)]
203
+ mod tests {
204
+ #![allow(clippy::unwrap_used, clippy::panic, clippy::expect_used)]
205
+ use super::*;
206
+ use serde_json::json;
207
+ use std::sync::atomic::{AtomicU32, Ordering};
208
+
209
+ static SEQ: AtomicU32 = AtomicU32::new(0);
210
+ fn temp_ws() -> PathBuf {
211
+ let n = SEQ.fetch_add(1, Ordering::Relaxed);
212
+ let ws = std::env::temp_dir().join(format!("ta_rs_ev_{}_{}", std::process::id(), n));
213
+ std::fs::create_dir_all(&ws).unwrap();
214
+ ws
215
+ }
216
+
217
+ // 确定性字节对拍:to_python_json(sort_value(..)) == Python json.dumps(sort_keys,ensure_ascii=False)。
218
+ #[test]
219
+ fn python_json_format_byte_parity() {
220
+ let v = json!({"event":"u","msg":"héllo🦀\n世界","nested":{"b":2,"a":[1,2]}});
221
+ assert_eq!(
222
+ to_python_json(&sort_value(&v)),
223
+ r#"{"event": "u", "msg": "héllo🦀\n世界", "nested": {"a": [1, 2], "b": 2}}"#
224
+ );
225
+ // 空 fields。
226
+ assert_eq!(to_python_json(&sort_value(&json!({"event":"empty"}))), r#"{"event": "empty"}"#);
227
+ // 类型:bool/null/int 与 Python 一致。
228
+ assert_eq!(
229
+ to_python_json(&sort_value(&json!({"missing":false,"x":null,"n":2}))),
230
+ r#"{"missing": false, "n": 2, "x": null}"#
231
+ );
232
+ }
233
+
234
+ #[test]
235
+ fn write_sorts_keys_and_has_ts_event() {
236
+ let ws = temp_ws();
237
+ let log = EventLog::new(&ws);
238
+ log.write("schema.layout_rebuild", json!({"table":"messages","row_count_before":2,"row_count_after":2,"missing":false})).unwrap();
239
+ let line = std::fs::read_to_string(ws.join(".team/logs/events.jsonl")).unwrap();
240
+ let line = line.trim_end();
241
+ // 键全排序:event < missing < row_count_after < row_count_before < table < ts。
242
+ assert!(line.starts_with(r#"{"event": "schema.layout_rebuild", "missing": false, "row_count_after": 2, "row_count_before": 2, "table": "messages", "ts": "#));
243
+ // ts 是合法 rfc3339 UTC。
244
+ let v: Value = serde_json::from_str(line).unwrap();
245
+ let ts = v["ts"].as_str().unwrap();
246
+ assert!(chrono::DateTime::parse_from_rfc3339(ts).is_ok(), "ts 非合法 rfc3339: {ts}");
247
+ assert!(ts.ends_with("+00:00"));
248
+ }
249
+
250
+ #[test]
251
+ fn tail_returns_last_n_and_raw_on_bad_line() {
252
+ let ws = temp_ws();
253
+ let log = EventLog::new(&ws);
254
+ for i in 0..5 {
255
+ log.write("e", json!({"i":i})).unwrap();
256
+ }
257
+ // 追加一行坏 JSON。
258
+ let p = ws.join(".team/logs/events.jsonl");
259
+ let mut f = std::fs::OpenOptions::new().append(true).open(&p).unwrap();
260
+ f.write_all(b"not json\n").unwrap();
261
+ drop(f);
262
+ let t = log.tail(2).unwrap();
263
+ assert_eq!(t.len(), 2);
264
+ assert_eq!(t[0]["i"], json!(4));
265
+ assert_eq!(t[1]["raw"], json!("not json"));
266
+ // 不存在 → 空。
267
+ assert!(EventLog::new(&temp_ws()).tail(10).unwrap().is_empty());
268
+ }
269
+
270
+ #[test]
271
+ fn rotation_at_threshold_shifts_archives_and_drops_oldest() {
272
+ let ws = temp_ws();
273
+ let p = logs_dir(&ws).join("events.jsonl");
274
+ std::fs::create_dir_all(p.parent().unwrap()).unwrap();
275
+ let log = EventLog::at(p.clone());
276
+ // 预置 current >= 5MiB + 已有 .1..=.5 archive(各带标记内容)。
277
+ std::fs::write(&p, vec![b'x'; EVENT_LOG_ROTATE_BYTES as usize]).unwrap();
278
+ for i in 1..=EVENT_LOG_ARCHIVE_KEEP {
279
+ std::fs::write(p.with_file_name(format!("events.jsonl.{i}")), format!("arc{i}")).unwrap();
280
+ }
281
+ // 下一次 write 触发轮转:.5 丢弃,.4→.5 ... .1→.2,current→.1。
282
+ log.write("after.rotate", json!({})).unwrap();
283
+ // current 现在是轮转后的新文件,只含刚写的一条。
284
+ let new_current = std::fs::read_to_string(&p).unwrap();
285
+ assert_eq!(new_current.lines().count(), 1);
286
+ assert!(new_current.contains("after.rotate"));
287
+ // .1 = 旧 current(5MiB 的 x)。
288
+ assert_eq!(std::fs::metadata(p.with_file_name("events.jsonl.1")).unwrap().len(), EVENT_LOG_ROTATE_BYTES);
289
+ // .2 = 旧 .1(内容 "arc1");.5 = 旧 .4("arc4");旧 .5("arc5")被丢弃。
290
+ assert_eq!(std::fs::read_to_string(p.with_file_name("events.jsonl.2")).unwrap(), "arc1");
291
+ assert_eq!(std::fs::read_to_string(p.with_file_name("events.jsonl.5")).unwrap(), "arc4");
292
+ }
293
+
294
+ #[test]
295
+ fn no_rotation_below_threshold() {
296
+ let ws = temp_ws();
297
+ let log = EventLog::new(&ws);
298
+ log.write("small", json!({})).unwrap();
299
+ log.write("small2", json!({})).unwrap();
300
+ // 未超阈值 → 无 .1 archive。
301
+ assert!(!ws.join(".team/logs/events.jsonl.1").exists());
302
+ assert_eq!(log.tail(10).unwrap().len(), 2);
303
+ }
304
+
305
+ // 对抗 P4:ts micros==0 省略小数秒(Python isoformat);否则 6 位微秒。golden 自 Python。
306
+ #[test]
307
+ fn format_ts_matches_python_isoformat() {
308
+ let m0 = chrono::DateTime::from_timestamp(1_780_000_000, 0).unwrap();
309
+ assert_eq!(format_ts(m0), "2026-05-28T20:26:40+00:00");
310
+ let m = chrono::DateTime::from_timestamp(1_780_000_000, 123_456_000).unwrap();
311
+ assert_eq!(format_ts(m), "2026-05-28T20:26:40.123456+00:00");
312
+ }
313
+
314
+ // 对抗 P7:tail(0) 复刻 Python lines[-0:] = 全部(负零切片怪癖)。
315
+ #[test]
316
+ fn tail_zero_returns_all_limit_clamps() {
317
+ let ws = temp_ws();
318
+ let log = EventLog::new(&ws);
319
+ for i in 0..3 {
320
+ log.write("e", json!({ "i": i })).unwrap();
321
+ }
322
+ assert_eq!(log.tail(0).unwrap().len(), 3, "tail(0) == 全部(Python [-0:])");
323
+ assert_eq!(log.tail(2).unwrap().len(), 2);
324
+ assert_eq!(log.tail(99).unwrap().len(), 3);
325
+ }
326
+
327
+ // 对抗 P1:多写者并发 append 不交错(单次 write_all 原子)。每行须是完整合法 JSON。
328
+ #[test]
329
+ fn concurrent_appends_do_not_interleave() {
330
+ let ws = temp_ws();
331
+ let path = logs_dir(&ws).join("events.jsonl");
332
+ std::fs::create_dir_all(path.parent().unwrap()).unwrap();
333
+ let threads: Vec<_> = (0..8)
334
+ .map(|t| {
335
+ let p = path.clone();
336
+ std::thread::spawn(move || {
337
+ let log = EventLog::at(p);
338
+ for i in 0..50 {
339
+ log.write("concurrent", json!({ "t": t, "i": i })).unwrap();
340
+ }
341
+ })
342
+ })
343
+ .collect();
344
+ for h in threads {
345
+ h.join().unwrap();
346
+ }
347
+ let text = std::fs::read_to_string(&path).unwrap();
348
+ let lines: Vec<&str> = text.lines().collect();
349
+ assert_eq!(lines.len(), 8 * 50, "无半行/丢行");
350
+ for line in &lines {
351
+ // 每行完整合法 JSON 且含 event 键 → 未交错。
352
+ let v: Value = serde_json::from_str(line).unwrap_or_else(|e| panic!("交错坏行: {line:?} ({e})"));
353
+ assert_eq!(v["event"], json!("concurrent"));
354
+ }
355
+ }
356
+
357
+ // 对抗 P5:真 events.jsonl golden(60 行 Python 序列化)逐行 round-trip 字节对拍。
358
+ #[test]
359
+ fn real_events_jsonl_fixture_round_trips_byte_identical() {
360
+ let fixture = include_str!(concat!(
361
+ env!("CARGO_MANIFEST_DIR"),
362
+ "/../../snapshot/fixtures/bug_082_codex_trust/macmini_0210_own_events_after_corrected_send.jsonl"
363
+ ));
364
+ let mut n = 0;
365
+ for line in fixture.lines() {
366
+ if line.is_empty() {
367
+ continue;
368
+ }
369
+ let v: Value = serde_json::from_str(line).unwrap();
370
+ assert_eq!(to_python_json(&sort_value(&v)), line, "第 {n} 行 round-trip 不字节一致");
371
+ n += 1;
372
+ }
373
+ assert_eq!(n, 60, "fixture 应 60 行");
374
+ }
375
+ }
@@ -0,0 +1,253 @@
1
+ //! `team-agent fake-worker` — Rust port of Python `team_agent.fake_worker` (SKELETON).
2
+ //!
3
+ //! Truth source (READ-ONLY) `team-agent-public` @ v0.2.11: `src/team_agent/fake_worker.py`.
4
+ //! A subscription-free backing program for `Provider::Fake`: it lets the real spawn path
5
+ //! (`launch(dry_run=false)` → tmux window) be exercised with NO real provider/subscription, so the
6
+ //! acceptance framework's cheap real-machine Tier-1 can drive the real daemon.
7
+ //!
8
+ //! Behavior (port target):
9
+ //! - print `TEAM_AGENT_FAKE_READY agent=<id>` (idle marker the daemon's classifier recognizes).
10
+ //! - read input line-by-line; on a non-empty line print `TEAM_AGENT_FAKE_WORKING agent=<id>`;
11
+ //! a `TEAM_AGENT_MESSAGE <json>` line OR a rendered `Team Agent message from …:` block (ending
12
+ //! `[team-agent-token:<tok>]`) → build a `result_envelope_v1` and report it via
13
+ //! `messaging::report_result`, echoing the envelope JSON; then print READY again.
14
+ //!
15
+ //! §84/MUST-NOT-13: a fake worker, no provider client. §10: implementation must be panic-free
16
+ //! (porter adds the deny + body; this skeleton is `unimplemented!()`).
17
+ #![allow(dead_code)]
18
+ #![cfg_attr(
19
+ not(test),
20
+ deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)
21
+ )]
22
+
23
+ use std::io::{BufRead, Write};
24
+ use std::path::Path;
25
+
26
+ use thiserror::Error;
27
+
28
+ #[derive(Debug, Error)]
29
+ pub enum FakeWorkerError {
30
+ #[error("io: {0}")]
31
+ Io(#[from] std::io::Error),
32
+ #[error("json: {0}")]
33
+ Json(#[from] serde_json::Error),
34
+ #[error("report_result: {0}")]
35
+ Report(String),
36
+ }
37
+
38
+ /// Run the fake-worker loop against `input`/`output` (stdin/stdout in `main`, in-memory in tests).
39
+ /// Port of `fake_worker.py:main` — the argv parsing (`--workspace`/`--agent-id`) lives in the
40
+ /// `fake-worker` subcommand dispatch; this entry takes them already resolved so it is unit-testable.
41
+ pub fn run(
42
+ workspace: &Path,
43
+ agent_id: &str,
44
+ input: impl BufRead,
45
+ mut output: impl Write,
46
+ ) -> Result<(), FakeWorkerError> {
47
+ writeln!(output, "TEAM_AGENT_FAKE_READY agent={agent_id}")?;
48
+ let mut block: Option<RenderedBlock> = None;
49
+ for line in input.lines() {
50
+ let line = line?;
51
+ let trimmed = line.trim();
52
+ if trimmed.is_empty() {
53
+ if let Some(block) = block.as_mut() {
54
+ block.lines.push(String::new());
55
+ }
56
+ continue;
57
+ }
58
+
59
+ writeln!(output, "TEAM_AGENT_FAKE_WORKING agent={agent_id}")?;
60
+ if let Some(raw) = trimmed.strip_prefix("TEAM_AGENT_MESSAGE ") {
61
+ let payload: serde_json::Value = serde_json::from_str(raw)?;
62
+ let message_id = payload
63
+ .get("message_id")
64
+ .and_then(serde_json::Value::as_str)
65
+ .unwrap_or("unknown");
66
+ let task_id = payload.get("task_id").and_then(serde_json::Value::as_str);
67
+ report_fake_result(workspace, agent_id, message_id, task_id, &mut output)?;
68
+ block = None;
69
+ } else if let Some(parsed) = parse_rendered_header(trimmed) {
70
+ block = Some(parsed);
71
+ } else if let Some(token) = parse_token(trimmed) {
72
+ if let Some(current) = block.take() {
73
+ let _content = current.content();
74
+ report_fake_result(
75
+ workspace,
76
+ agent_id,
77
+ &token,
78
+ current.task_id.as_deref(),
79
+ &mut output,
80
+ )?;
81
+ }
82
+ } else if let Some(current) = block.as_mut() {
83
+ current.lines.push(trimmed.to_string());
84
+ }
85
+ writeln!(output, "TEAM_AGENT_FAKE_READY agent={agent_id}")?;
86
+ }
87
+ Ok(())
88
+ }
89
+
90
+ #[derive(Debug)]
91
+ struct RenderedBlock {
92
+ task_id: Option<String>,
93
+ lines: Vec<String>,
94
+ }
95
+
96
+ impl RenderedBlock {
97
+ fn content(&self) -> String {
98
+ self.lines.join("\n").trim().to_string()
99
+ }
100
+ }
101
+
102
+ fn parse_rendered_header(line: &str) -> Option<RenderedBlock> {
103
+ let rest = line.strip_prefix("Team Agent message from ")?;
104
+ let header = rest.strip_suffix(':')?;
105
+ let task_id = header
106
+ .split_once(" for ")
107
+ .map(|(_, task)| task.trim().to_string())
108
+ .filter(|task| !task.is_empty());
109
+ Some(RenderedBlock { task_id, lines: Vec::new() })
110
+ }
111
+
112
+ fn parse_token(line: &str) -> Option<String> {
113
+ let token = line
114
+ .strip_prefix("[team-agent-token:")
115
+ .and_then(|rest| rest.strip_suffix(']'))?
116
+ .trim();
117
+ if token.is_empty() {
118
+ None
119
+ } else {
120
+ Some(token.to_string())
121
+ }
122
+ }
123
+
124
+ fn report_fake_result(
125
+ workspace: &Path,
126
+ agent_id: &str,
127
+ message_id: &str,
128
+ task_id: Option<&str>,
129
+ output: &mut impl Write,
130
+ ) -> Result<(), FakeWorkerError> {
131
+ let envelope = fake_envelope(workspace, agent_id, message_id, task_id);
132
+ crate::messaging::report_result(workspace, &envelope)
133
+ .map_err(|e| FakeWorkerError::Report(e.to_string()))?;
134
+ writeln!(output, "{}", serde_json::to_string(&envelope)?)?;
135
+ Ok(())
136
+ }
137
+
138
+ fn fake_envelope(
139
+ workspace: &Path,
140
+ agent_id: &str,
141
+ message_id: &str,
142
+ task_id: Option<&str>,
143
+ ) -> serde_json::Value {
144
+ serde_json::json!({
145
+ "schema_version": "result_envelope_v1",
146
+ "task_id": task_id.unwrap_or("manual"),
147
+ "agent_id": agent_id,
148
+ "status": "success",
149
+ "summary": format!("Fake worker handled message {message_id}"),
150
+ "changes": [],
151
+ "tests": [{"command": "fake-provider", "status": "passed"}],
152
+ "risks": [],
153
+ "artifacts": [{
154
+ "path": workspace.join(".team").join("logs").join(format!("{agent_id}.scrollback")).to_string_lossy(),
155
+ "description": "tmux scrollback for fake worker"
156
+ }],
157
+ "next_actions": []
158
+ })
159
+ }
160
+
161
+ #[cfg(test)]
162
+ mod tests {
163
+ //! FAKE-WORKER RED — `run` is the `unimplemented!()` skeleton today, so these PANIC (RED) until the
164
+ //! porter ports `fake_worker.py`. Golden captured live from
165
+ //! `PYTHONPATH=…/team-agent-public/src python3 -m team_agent.fake_worker --workspace <ws> --agent-id w1`:
166
+ //! both the `TEAM_AGENT_MESSAGE {json}` line and the rendered `Team Agent message from leader for t1:`
167
+ //! block (ending `[team-agent-token:m1]`) report the SAME `result_envelope_v1` via the real
168
+ //! `messaging::report_result`, and the daemon-recognizable READY/WORKING markers are printed.
169
+ use super::run;
170
+ use std::io::Cursor;
171
+ use std::sync::atomic::{AtomicU64, Ordering};
172
+
173
+ use crate::db::schema::open_db;
174
+ use crate::message_store::MessageStore;
175
+
176
+ fn fake_ws() -> std::path::PathBuf {
177
+ static N: AtomicU64 = AtomicU64::new(0);
178
+ let dir = std::env::temp_dir().join(format!(
179
+ "ta-rs-fakew-{}-{}",
180
+ std::process::id(),
181
+ N.fetch_add(1, Ordering::Relaxed)
182
+ ));
183
+ std::fs::create_dir_all(&dir).unwrap();
184
+ dir
185
+ }
186
+
187
+ /// Read back the single result the fake worker stored via `messaging::report_result`.
188
+ fn stored_result(ws: &std::path::Path) -> (String, String, String, serde_json::Value) {
189
+ let store = MessageStore::open(ws).unwrap();
190
+ let conn = open_db(store.db_path()).unwrap();
191
+ let mut stmt = conn.prepare("select task_id, agent_id, status, envelope from results").unwrap();
192
+ let row = stmt
193
+ .query_row([], |r| {
194
+ Ok((
195
+ r.get::<_, String>(0)?,
196
+ r.get::<_, String>(1)?,
197
+ r.get::<_, String>(2)?,
198
+ r.get::<_, String>(3)?,
199
+ ))
200
+ })
201
+ .expect("exactly one stored fake-worker result");
202
+ let envelope: serde_json::Value = serde_json::from_str(&row.3).unwrap();
203
+ (row.0, row.1, row.2, envelope)
204
+ }
205
+
206
+ /// Assert the stored envelope matches the captured Python golden (message_id m1, task_id t1).
207
+ fn assert_golden_envelope(ws: &std::path::Path) {
208
+ let (task_id, agent_id, status, env) = stored_result(ws);
209
+ assert_eq!(
210
+ (task_id.as_str(), agent_id.as_str(), status.as_str()),
211
+ ("t1", "w1", "success"),
212
+ "stored result row must be task_id=t1 agent_id=w1 status=success"
213
+ );
214
+ assert_eq!(env["schema_version"], serde_json::json!("result_envelope_v1"));
215
+ assert_eq!(env["summary"], serde_json::json!("Fake worker handled message m1"));
216
+ assert_eq!(env["tests"], serde_json::json!([{"command": "fake-provider", "status": "passed"}]));
217
+ let artifact_path = env["artifacts"][0]["path"].as_str().unwrap_or_default();
218
+ assert!(
219
+ artifact_path.ends_with(".team/logs/w1.scrollback"),
220
+ "artifact path must be <ws>/.team/logs/w1.scrollback; got {artifact_path}"
221
+ );
222
+ }
223
+
224
+ // TEAM_AGENT_MESSAGE form: `TEAM_AGENT_MESSAGE {json}` line → real report_result stores the golden
225
+ // result_envelope_v1; READY (idle) + WORKING markers printed. Golden _report_fake_result.
226
+ #[test]
227
+ fn fake_worker_team_agent_message_reports_golden_envelope() {
228
+ let ws = fake_ws();
229
+ let input = "TEAM_AGENT_MESSAGE {\"message_id\":\"m1\",\"task_id\":\"t1\"}\n";
230
+ let mut out: Vec<u8> = Vec::new();
231
+ run(&ws, "w1", Cursor::new(input.as_bytes()), &mut out).expect("fake worker run");
232
+
233
+ assert_golden_envelope(&ws);
234
+ let printed = String::from_utf8(out).unwrap();
235
+ assert!(printed.contains("TEAM_AGENT_FAKE_READY agent=w1"), "must print the READY idle marker; got {printed}");
236
+ assert!(printed.contains("TEAM_AGENT_FAKE_WORKING agent=w1"), "must print the WORKING marker on a non-empty line; got {printed}");
237
+ }
238
+
239
+ // Rendered-message form: a `Team Agent message from leader for t1:` block ending `[team-agent-token:m1]`
240
+ // parses to the SAME envelope (message_id m1 from the token, task_id t1 from `for t1`). Golden
241
+ // _parse_rendered_message + _report_fake_result.
242
+ #[test]
243
+ fn fake_worker_rendered_message_reports_same_golden_envelope() {
244
+ let ws = fake_ws();
245
+ let input = "Team Agent message from leader for t1:\nplease do X\n[team-agent-token:m1]\n";
246
+ let mut out: Vec<u8> = Vec::new();
247
+ run(&ws, "w1", Cursor::new(input.as_bytes()), &mut out).expect("fake worker run");
248
+
249
+ assert_golden_envelope(&ws);
250
+ let printed = String::from_utf8(out).unwrap();
251
+ assert!(printed.contains("TEAM_AGENT_FAKE_WORKING agent=w1"), "rendered form prints WORKING per line; got {printed}");
252
+ }
253
+ }