swarph-cli 0.40.0__tar.gz → 0.40.1__tar.gz

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 (222) hide show
  1. {swarph_cli-0.40.0/src/swarph_cli.egg-info → swarph_cli-0.40.1}/PKG-INFO +1 -1
  2. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/pyproject.toml +1 -1
  3. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/__init__.py +1 -1
  4. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/daemon.py +89 -18
  5. {swarph_cli-0.40.0 → swarph_cli-0.40.1/src/swarph_cli.egg-info}/PKG-INFO +1 -1
  6. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli.egg-info/SOURCES.txt +2 -0
  7. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_brain_ask_command.py +1 -1
  8. swarph_cli-0.40.1/tests/test_console_encoding_e2e.py +92 -0
  9. swarph_cli-0.40.1/tests/test_daemon_console_encoding.py +174 -0
  10. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_watchdog.py +1 -1
  11. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/LICENSE +0 -0
  12. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/README.md +0 -0
  13. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/setup.cfg +0 -0
  14. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/bench/__init__.py +0 -0
  15. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/bench/backends.py +0 -0
  16. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/bench/pack.py +0 -0
  17. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/bench/prices.py +0 -0
  18. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/bench/quality.py +0 -0
  19. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/bench/refresh_prices.py +0 -0
  20. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/bench/runner.py +0 -0
  21. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/bench/validate.py +0 -0
  22. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/caller.py +0 -0
  23. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/capture/__init__.py +0 -0
  24. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/capture/harden.py +0 -0
  25. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/capture/lineage.py +0 -0
  26. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/capture/liveness.py +0 -0
  27. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/capture/manifest.py +0 -0
  28. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/capture/paths.py +0 -0
  29. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/capture/verify.py +0 -0
  30. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/cell.py +0 -0
  31. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/chain_token.py +0 -0
  32. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/__init__.py +0 -0
  33. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/_display.py +0 -0
  34. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/_gateway_client.py +0 -0
  35. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/add.py +0 -0
  36. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/bench.py +0 -0
  37. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/board.py +0 -0
  38. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/board_merge_check.py +0 -0
  39. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/brain.py +0 -0
  40. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/brain_ask.py +0 -0
  41. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/cell.py +0 -0
  42. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/cell_selfcheck.py +0 -0
  43. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/channel.py +0 -0
  44. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/chat.py +0 -0
  45. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/codegraph.py +0 -0
  46. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/compress.py +0 -0
  47. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/event.py +0 -0
  48. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/gateway.py +0 -0
  49. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/group.py +0 -0
  50. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/highlight.py +0 -0
  51. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/hook_output.py +0 -0
  52. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/hooks.py +0 -0
  53. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/import_session.py +0 -0
  54. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/init.py +0 -0
  55. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/install_hook.py +0 -0
  56. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/install_multiplexer.py +0 -0
  57. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/lane.py +0 -0
  58. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/mcp_server.py +0 -0
  59. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/memory.py +0 -0
  60. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/memory_sync.py +0 -0
  61. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/mesh.py +0 -0
  62. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/monitor.py +0 -0
  63. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/okf_links.py +0 -0
  64. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/onboard.py +0 -0
  65. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/protocol_handler.py +0 -0
  66. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/ratify.py +0 -0
  67. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/schedule.py +0 -0
  68. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/security.py +0 -0
  69. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/service.py +0 -0
  70. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/spawn.py +0 -0
  71. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/timeline.py +0 -0
  72. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/watchdog.py +0 -0
  73. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/compress/__init__.py +0 -0
  74. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/compress/levers.py +0 -0
  75. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/compress/marker.py +0 -0
  76. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/compress/verify.py +0 -0
  77. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/delivery_queue.py +0 -0
  78. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/gateway/__init__.py +0 -0
  79. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/gateway/feature_registry.py +0 -0
  80. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/gateway/lanes_control.py +0 -0
  81. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/gateway/schema.sql +0 -0
  82. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/gateway/server.py +0 -0
  83. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/gateway/services_control.py +0 -0
  84. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/main.py +0 -0
  85. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/multiplexer.py +0 -0
  86. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/parsers/__init__.py +0 -0
  87. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/parsers/claude.py +0 -0
  88. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/scripts/ensure_monitor.sh +0 -0
  89. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/service/__init__.py +0 -0
  90. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/service/app.py +0 -0
  91. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/service/providers.py +0 -0
  92. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/session_bridge.py +0 -0
  93. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/stall_alert.py +0 -0
  94. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
  95. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
  96. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
  97. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
  98. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli.egg-info/entry_points.txt +0 -0
  99. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli.egg-info/requires.txt +0 -0
  100. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli.egg-info/top_level.txt +0 -0
  101. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_add_security_gate.py +0 -0
  102. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_artifact_add.py +0 -0
  103. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_artifact_hash.py +0 -0
  104. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_artifact_lib.py +0 -0
  105. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_artifact_mcp.py +0 -0
  106. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_artifact_skill.py +0 -0
  107. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_artifact_tool.py +0 -0
  108. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_artifact_uri.py +0 -0
  109. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_bench_backends.py +0 -0
  110. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_bench_command.py +0 -0
  111. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_bench_pack.py +0 -0
  112. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_bench_prices.py +0 -0
  113. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_bench_quality.py +0 -0
  114. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_bench_runner.py +0 -0
  115. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_bench_validate.py +0 -0
  116. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_board_command.py +0 -0
  117. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_board_merge_check.py +0 -0
  118. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_brain_ask_env_diagnosis.py +0 -0
  119. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_brain_command.py +0 -0
  120. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_caller_meta_guard.py +0 -0
  121. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_capture_lineage.py +0 -0
  122. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_capture_liveness.py +0 -0
  123. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_capture_manifest.py +0 -0
  124. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_capture_paths.py +0 -0
  125. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_capture_security.py +0 -0
  126. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_cell_command_dispatch.py +0 -0
  127. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_cell_harden.py +0 -0
  128. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_cell_loader.py +0 -0
  129. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_cell_selfcheck.py +0 -0
  130. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_cell_verify.py +0 -0
  131. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_chain_token.py +0 -0
  132. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_channel_command.py +0 -0
  133. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_channel_post.py +0 -0
  134. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_chat_command.py +0 -0
  135. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_claude_parser.py +0 -0
  136. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_claude_tmux_template.py +0 -0
  137. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_codegraph_command.py +0 -0
  138. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_codegraph_mcp.py +0 -0
  139. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_compress_caller_convention.py +0 -0
  140. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_compress_command.py +0 -0
  141. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_compress_levers.py +0 -0
  142. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_compress_marker.py +0 -0
  143. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_compress_verify.py +0 -0
  144. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_daemon_command.py +0 -0
  145. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_daemon_delivery.py +0 -0
  146. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_delivery_queue.py +0 -0
  147. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_display_sanitize.py +0 -0
  148. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_event_emit.py +0 -0
  149. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_feature_to_uri.py +0 -0
  150. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_gateway_brain.py +0 -0
  151. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_gateway_client.py +0 -0
  152. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_gateway_command.py +0 -0
  153. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_group_command.py +0 -0
  154. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_highlight_command.py +0 -0
  155. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_hook_output.py +0 -0
  156. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_hooks_activity_marker.py +0 -0
  157. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_hooks_add.py +0 -0
  158. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_hooks_bundle.py +0 -0
  159. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_hooks_init.py +0 -0
  160. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_hooks_lifecycle.py +0 -0
  161. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_hooks_merge.py +0 -0
  162. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_import_command.py +0 -0
  163. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_init_command.py +0 -0
  164. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_install_hook.py +0 -0
  165. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_install_multiplexer_command.py +0 -0
  166. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_lane_command.py +0 -0
  167. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_main.py +0 -0
  168. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_main_unknown_verb_guard.py +0 -0
  169. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_mcp_server.py +0 -0
  170. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_memory_command.py +0 -0
  171. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_memory_gateway.py +0 -0
  172. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_memory_sync.py +0 -0
  173. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_memory_sync_membrane.py +0 -0
  174. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_mesh_command.py +0 -0
  175. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_mesh_inbox_mark_read.py +0 -0
  176. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_mesh_inbox_sanitize.py +0 -0
  177. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_mesh_sidecar.py +0 -0
  178. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_mesh_sidecar_cursor_decoupled.py +0 -0
  179. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_mesh_sidecar_wake_survives_read.py +0 -0
  180. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_meta_edge_identity.py +0 -0
  181. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_monitor_cli.py +0 -0
  182. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_monitor_daemon_collision.py +0 -0
  183. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_monitor_self_name_verification.py +0 -0
  184. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_monitor_state_split.py +0 -0
  185. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_multiplexer.py +0 -0
  186. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_okf_links.py +0 -0
  187. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_onboard_command.py +0 -0
  188. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_packaged_artifacts_exist.py +0 -0
  189. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_protocol_handler.py +0 -0
  190. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_providers_isolation.py +0 -0
  191. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_ratify_command.py +0 -0
  192. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_repl_caller_convention.py +0 -0
  193. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_schedule_command.py +0 -0
  194. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_security.py +0 -0
  195. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_service_app.py +0 -0
  196. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_service_command.py +0 -0
  197. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_service_providers.py +0 -0
  198. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_session_bridge_inject.py +0 -0
  199. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_session_bridge_probe.py +0 -0
  200. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_session_bridge_resolve.py +0 -0
  201. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_session_path_role_gate.py +0 -0
  202. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_shipped_units_match_the_cli.py +0 -0
  203. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_smoke_chat.py +0 -0
  204. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_smoke_one_shot.py +0 -0
  205. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_smoke_phase_5_5.py +0 -0
  206. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_spawn_base_hoist.py +0 -0
  207. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_spawn_command.py +0 -0
  208. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_spawn_live_pin.py +0 -0
  209. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_spawn_mitosis_lineage.py +0 -0
  210. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_spawn_tmux_session.py +0 -0
  211. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_spawn_windows_relaunch.py +0 -0
  212. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_stall_alert.py +0 -0
  213. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_timeline_command.py +0 -0
  214. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_token_file_one_parser.py +0 -0
  215. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_version_consistency.py +0 -0
  216. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_watchdog_dm_wake.py +0 -0
  217. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
  218. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_watchdog_dm_wake_wiring.py +0 -0
  219. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_watchdog_emit_health.py +0 -0
  220. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_watchdog_install_flags.py +0 -0
  221. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_watchdog_model_rung.py +0 -0
  222. {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_watchdog_stale_peers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swarph-cli
3
- Version: 0.40.0
3
+ Version: 0.40.1
4
4
  Summary: The `swarph` binary — a multi-LLM CLI and mesh-gateway client + bundled server (`swarph gateway`): one-shot prompts across providers, interactive `chat`, multi-provider `spawn` (claude/codex/antigravity via a ProviderMembrane with subprocess billing-scrub), `mesh` send/inbox/register, `brain-ask` over the swarph-brain (gbrain) semantic memory, git-backed `assisted_memory`, session `import`, and a stranded-session `watchdog`.
5
5
  Author: Pierre Samson, Claude Opus
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "swarph-cli"
7
- version = "0.40.0"
7
+ version = "0.40.1"
8
8
  description = "The `swarph` binary — a multi-LLM CLI and mesh-gateway client + bundled server (`swarph gateway`): one-shot prompts across providers, interactive `chat`, multi-provider `spawn` (claude/codex/antigravity via a ProviderMembrane with subprocess billing-scrub), `mesh` send/inbox/register, `brain-ask` over the swarph-brain (gbrain) semantic memory, git-backed `assisted_memory`, session `import`, and a stranded-session `watchdog`."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -16,6 +16,6 @@ The architecture splits CLI from substrate so:
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
- __version__ = "0.40.0"
19
+ __version__ = "0.40.1"
20
20
 
21
21
  __all__ = ["__version__"]
@@ -170,7 +170,7 @@ def _write_daemon_pidfile(state_dir: Path) -> Optional[Path]:
170
170
  os.replace(tmp, path)
171
171
  return path
172
172
  except OSError as exc:
173
- print(f"[swarph-daemon] could not write {DAEMON_PIDFILE}: {exc}",
173
+ _print_safe(f"[swarph-daemon] could not write {DAEMON_PIDFILE}: {exc}",
174
174
  file=sys.stderr, flush=True)
175
175
  return None
176
176
 
@@ -182,7 +182,7 @@ def _read_cursor(path: Path) -> dict:
182
182
  return json.loads(path.read_text(encoding="utf-8"))
183
183
  except json.JSONDecodeError as exc:
184
184
  # Loud — corrupted cursor needs operator attention, not silent reset.
185
- print(
185
+ _print_safe(
186
186
  f"[swarph-daemon] CORRUPTED cursor at {path}: {exc}. "
187
187
  f"Refusing to overwrite. Inspect manually.",
188
188
  file=sys.stderr,
@@ -260,16 +260,87 @@ class DaemonState:
260
260
 
261
261
  def _log_dm(state: DaemonState, dm: dict) -> None:
262
262
  """Both stdout (visible to operator + journald) AND inbox.log (cursor
263
- audit trail). Inbox.log is append-only structured JSONL."""
263
+ audit trail). Inbox.log is append-only structured JSONL.
264
+
265
+ >>> THE AUDIT WRITE COMES FIRST AND THE OPERATOR LINE CANNOT KILL THE LOOP. <<<
266
+ MEASURED on workstation-lc 2026-07-29 (French Windows, cp1252 console, Task
267
+ Scheduler): this function printed a hardcoded U+2192 to a charmap stdout, raising
268
+ UnicodeEncodeError on EVERY DM. `_drain_iteration` aborted, THE CURSOR NEVER
269
+ ADVANCED, ZERO DMS WERE DELIVERED — and the outer handler swallowed it as
270
+ "iteration error (continuing)", so Get-ScheduledTask reported Running and every
271
+ surface was green. THE CELL WAS DM-BLIND WITH NOTHING ANYWHERE SAYING SO.
272
+
273
+ Three separate faults, fixed separately because each is sufficient alone:
274
+ 1. the separator was a hardcoded non-ASCII character -> now ASCII "->"
275
+ 2. DM CONTENT is arbitrary (French, emoji, arrows) and reprs unescaped on py3,
276
+ so the console can still refuse it -> the write is encode-safe
277
+ 3. >>> A DISPLAY FAILURE MUST NEVER BE ABLE TO STOP DELIVERY. <<< The audit
278
+ trail is written FIRST, and the operator line is best-effort. Losing a
279
+ console line is cosmetic; losing the cursor is a silent outage.
280
+ """
281
+ # 1. AUDIT FIRST — utf-8 explicitly, never the locale default. If anything below
282
+ # fails, the DM is already durably recorded and the cursor may safely advance.
283
+ state.inbox_log_path.parent.mkdir(parents=True, exist_ok=True)
284
+ with state.inbox_log_path.open("a", encoding="utf-8") as f:
285
+ f.write(json.dumps(dm) + "\n")
286
+
287
+ # 2. OPERATOR LINE — best-effort, ASCII separator, and encode-safe against a
288
+ # console that cannot represent the content.
264
289
  line = (
265
290
  f"[{dm.get('created_at', '?')}] "
266
291
  f"id={dm['id']} from={dm.get('from_node')} kind={dm.get('kind')} "
267
- f" {dm['content'][:120]!r}"
292
+ f"-> {dm['content'][:120]!r}"
268
293
  )
269
- print(line, flush=True)
270
- state.inbox_log_path.parent.mkdir(parents=True, exist_ok=True)
271
- with state.inbox_log_path.open("a", encoding="utf-8") as f:
272
- f.write(json.dumps(dm) + "\n")
294
+ _print_safe(line)
295
+
296
+
297
+ def _print_safe(line: str, *, stream=None, file=None, flush: bool = True) -> None:
298
+ """Print, degrading to a lossy transliteration rather than raising.
299
+
300
+ A console whose encoding cannot represent a DM (cp1252 on a French Windows box,
301
+ any 8-bit codepage) must cost a mangled character, never a poll iteration.
302
+
303
+ >>> EVERY output path in this module goes through here, not just the DM line. <<<
304
+ droplet measured the first version's coverage at 1 call site out of 13, and named
305
+ the one that matters: the delivery-error handler in `attempt_delivery`, whose own
306
+ comment promises it will never crash the loop. A raise inside an `except` block
307
+ propagates past the `try` entirely — so the handler reached ONLY when something has
308
+ already gone wrong was itself the crash, on precisely the text most likely to carry
309
+ our characters (an exception message).
310
+
311
+ cp1252 coverage of what this fleet actually emits, measured:
312
+ renders : em-dash, bullet, accented e, guillemet
313
+ RAISES : arrow, box-drawing, emoji, check-mark
314
+ Our DMs and operator lines are full of arrows and emoji, and the em-dash is FINE —
315
+ so the original DM-blindness was an arrow or an emoji, never punctuation.
316
+
317
+ A SYMBOL GREP MEASURES PRESENCE, NOT COVERAGE: `grep _print_safe` returns 1 whether
318
+ this guards one path or all thirteen. Coverage is asserted by a test that walks the
319
+ module's AST for unguarded `print(` calls, not by the symbol existing.
320
+ """
321
+ # `file=` is accepted as an alias for `stream=`, and `flush` is accepted and
322
+ # ignored (this always flushes). Deliberate: every call site here was a `print`,
323
+ # and a future author reaching for the print spelling must land in the guard
324
+ # rather than a TypeError — a TypeError in the delivery-error handler is the
325
+ # exact crash this function exists to prevent, merely with a different name.
326
+ out = stream if stream is not None else (file if file is not None else sys.stdout)
327
+ try:
328
+ print(line, file=out, flush=True)
329
+ except UnicodeEncodeError:
330
+ enc = getattr(out, "encoding", None) or "ascii"
331
+ try:
332
+ out.write(line.encode(enc, errors="replace").decode(enc, errors="replace"))
333
+ out.write("\n")
334
+ out.flush()
335
+ except (OSError, UnicodeEncodeError, ValueError):
336
+ # The fallback failing must not resurrect the crash it exists to prevent.
337
+ # Losing an operator line is survivable; losing the loop is not.
338
+ pass
339
+ except (OSError, ValueError):
340
+ # A closed/broken stream (detached service, full disk) is not a reason to stop
341
+ # draining — the audit write already succeeded. ValueError covers
342
+ # write-on-closed-file, which service teardown reaches.
343
+ pass
273
344
 
274
345
 
275
346
  def _route_to_handler(state: DaemonState, dm: dict) -> None:
@@ -343,7 +414,7 @@ def attempt_delivery(state: DaemonState) -> None:
343
414
  # inject failure → leave queued, retry next tick (no counter bump —
344
415
  # the cell was idle; a send failure is transient, not a stall).
345
416
  except Exception as exc: # noqa: BLE001 — bridge must never crash the loop
346
- print(f"[swarph-daemon] delivery error (continuing): "
417
+ _print_safe(f"[swarph-daemon] delivery error (continuing): "
347
418
  f"{type(exc).__name__}: {exc}", file=sys.stderr, flush=True)
348
419
 
349
420
 
@@ -380,7 +451,7 @@ async def _drain_iteration(state: DaemonState) -> None:
380
451
  state.disconnect_since = time.time()
381
452
  elapsed = time.time() - state.disconnect_since
382
453
  if elapsed > _LOUD_DISCONNECT_SECONDS:
383
- print(
454
+ _print_safe(
384
455
  f"[swarph-daemon] LOUD: gateway unreachable for "
385
456
  f"{elapsed:.0f}s — {body.get('detail', '?')}",
386
457
  file=sys.stderr,
@@ -390,14 +461,14 @@ async def _drain_iteration(state: DaemonState) -> None:
390
461
  if status >= 500:
391
462
  if state.disconnect_since is None:
392
463
  state.disconnect_since = time.time()
393
- print(
464
+ _print_safe(
394
465
  f"[swarph-daemon] gateway 5xx {status}: {body.get('detail', '?')}",
395
466
  file=sys.stderr,
396
467
  flush=True,
397
468
  )
398
469
  return
399
470
  if status >= 400:
400
- print(
471
+ _print_safe(
401
472
  f"[swarph-daemon] gateway {status}: {body.get('detail', '?')}",
402
473
  file=sys.stderr,
403
474
  flush=True,
@@ -434,7 +505,7 @@ async def _drain_loop(state: DaemonState) -> None:
434
505
  """Main loop. Returns on shutdown_requested. Exceptions in
435
506
  _drain_iteration are caught + logged + retried; only signal handlers
436
507
  set shutdown_requested."""
437
- print(
508
+ _print_safe(
438
509
  f"[swarph-daemon] starting: self={state.self_name} "
439
510
  f"gateway={state.gateway} poll={state.poll_s}s "
440
511
  f"state={state.state_dir} auto_act={state.auto_act} "
@@ -446,7 +517,7 @@ async def _drain_loop(state: DaemonState) -> None:
446
517
  try:
447
518
  await _drain_iteration(state)
448
519
  except Exception as exc: # noqa: BLE001 — loud-on-error per §16.4
449
- print(
520
+ _print_safe(
450
521
  f"[swarph-daemon] iteration error (continuing): "
451
522
  f"{type(exc).__name__}: {exc}",
452
523
  file=sys.stderr,
@@ -461,7 +532,7 @@ async def _drain_loop(state: DaemonState) -> None:
461
532
  break
462
533
  await asyncio.sleep(1)
463
534
 
464
- print(
535
+ _print_safe(
465
536
  f"[swarph-daemon] shutdown: iterations={state.iterations} "
466
537
  f"dms_seen={state.dms_seen} cursor.last_msg_id={state.cursor.get('last_msg_id', 0)}",
467
538
  flush=True,
@@ -474,7 +545,7 @@ def _install_signal_handlers(loop: asyncio.AbstractEventLoop, state: DaemonState
474
545
 
475
546
  def _handler(signum, frame): # noqa: ARG001
476
547
  if not state.shutdown_requested:
477
- print(
548
+ _print_safe(
478
549
  f"[swarph-daemon] signal {signum} received — draining + flushing cursor",
479
550
  flush=True,
480
551
  )
@@ -500,7 +571,7 @@ def run_daemon(argv: list[str]) -> int:
500
571
  elif self_name:
501
572
  state_dir = Path.home() / "swarph_state" / self_name
502
573
  else:
503
- print(
574
+ _print_safe(
504
575
  "swarph daemon: cannot resolve identity. Pass --self <name> or "
505
576
  "--state-dir <path> or set $SWARPH_SELF.",
506
577
  file=sys.stderr,
@@ -514,7 +585,7 @@ def run_daemon(argv: list[str]) -> int:
514
585
  _write_daemon_pidfile(state_dir)
515
586
  token = _resolve_token(args.token_file)
516
587
  if not token:
517
- print("swarph daemon: empty MESH_GATEWAY_TOKEN", file=sys.stderr)
588
+ _print_safe("swarph daemon: empty MESH_GATEWAY_TOKEN", file=sys.stderr)
518
589
  return 2
519
590
 
520
591
  state = DaemonState(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swarph-cli
3
- Version: 0.40.0
3
+ Version: 0.40.1
4
4
  Summary: The `swarph` binary — a multi-LLM CLI and mesh-gateway client + bundled server (`swarph gateway`): one-shot prompts across providers, interactive `chat`, multi-provider `spawn` (claude/codex/antigravity via a ProviderMembrane with subprocess billing-scrub), `mesh` send/inbox/register, `brain-ask` over the swarph-brain (gbrain) semantic memory, git-backed `assisted_memory`, session `import`, and a stranded-session `watchdog`.
5
5
  Author: Pierre Samson, Claude Opus
6
6
  License: MIT
@@ -136,7 +136,9 @@ tests/test_compress_command.py
136
136
  tests/test_compress_levers.py
137
137
  tests/test_compress_marker.py
138
138
  tests/test_compress_verify.py
139
+ tests/test_console_encoding_e2e.py
139
140
  tests/test_daemon_command.py
141
+ tests/test_daemon_console_encoding.py
140
142
  tests/test_daemon_delivery.py
141
143
  tests/test_delivery_queue.py
142
144
  tests/test_display_sanitize.py
@@ -17,7 +17,7 @@ from swarph_cli.commands import brain_ask as ba
17
17
 
18
18
  def test_version_is_0_33_0():
19
19
  import swarph_cli
20
- assert swarph_cli.__version__ == "0.40.0"
20
+ assert swarph_cli.__version__ == "0.40.1"
21
21
 
22
22
 
23
23
  # --- request / response plumbing -------------------------------------------
@@ -0,0 +1,92 @@
1
+ """END-TO-END: the CLI must survive an 8-bit console with a redirected stdout.
2
+
3
+ >>> THIS IS THE GAP THE WINDOWS CI LEGS CANNOT COVER, AND THE REASON IS STRUCTURAL. <<<
4
+ Under pytest, `sys.stdout` is a TextIOWrapper with encoding utf-8 and isatty()==False
5
+ ON EVERY PLATFORM — capture substitutes the very object that fails. So a
6
+ console-encoding fault is unreachable from inside a captured test, and adding more
7
+ Windows runners would not have changed that. MEASURED: pytest stdout is utf-8 on Linux
8
+ and on windows-latest alike.
9
+
10
+ These tests therefore spawn a REAL SUBPROCESS with a REAL 8-bit stdout, which is the
11
+ condition workstation-lc hit: French Windows (cp1252) + Task Scheduler, which REDIRECTS
12
+ stdout — and a redirected stream on Windows uses locale.getpreferredencoding(), not the
13
+ UTF-16 console API. `PYTHONIOENCODING=cp1252` reproduces that exactly, on Linux, in CI,
14
+ with no French Windows anywhere.
15
+
16
+ WHAT IT COST WHEN NOTHING CHECKED THIS: a hardcoded U+2192 in `_log_dm` raised on every
17
+ DM, `_drain_iteration` aborted, THE CURSOR FROZE AND ZERO DMS WERE DELIVERED — while
18
+ Get-ScheduledTask reported Running and every other surface was green.
19
+ """
20
+ import os
21
+ import subprocess
22
+ import sys
23
+ import textwrap
24
+
25
+ import pytest
26
+
27
+ # The exact shape of the operator line that failed, plus content a French cell sends.
28
+ _HOSTILE = "id=1 from=lab-ovh kind=answer -> 'réponse — arrow → and emoji ✅'"
29
+
30
+
31
+ def _run(code: str, encoding: str) -> subprocess.CompletedProcess:
32
+ """Run in a subprocess with a REDIRECTED, 8-bit stdout — Task Scheduler's shape."""
33
+ # THE PARENT MUST DECODE WHAT THE CHILD ACTUALLY WROTE. `text=True` decodes with
34
+ # the PARENT's locale (utf-8 here), so it raises on the child's cp1252 bytes — the
35
+ # harness for an encoding test having an encoding bug. Decode explicitly, and
36
+ # errors="replace" so a mangled byte is data rather than an exception.
37
+ return subprocess.run(
38
+ [sys.executable, "-c", textwrap.dedent(code)],
39
+ capture_output=True, timeout=60,
40
+ encoding=encoding, errors="replace",
41
+ # INHERIT the real environment and override only what this test controls.
42
+ # An earlier version passed env={"PATH": "/usr/bin:/bin", ...} — a POSIX path,
43
+ # in the test written to prove we handle a WINDOWS console. It failed both
44
+ # Windows legs: the interpreter cannot resolve, and SYSTEMROOT/COMSPEC are
45
+ # required for a subprocess there. >>> A CROSS-PLATFORM TEST THAT HARDCODES ONE
46
+ # PLATFORM'S PATH TESTS THE PLATFORM IT RUNS ON, WHICH IS THE ONE THAT WAS
47
+ # ALREADY FINE. <<<
48
+ env={**os.environ, "PYTHONIOENCODING": encoding, "PYTHONPATH": "src"},
49
+ )
50
+
51
+
52
+ def test_the_bug_reproduces_without_the_guard():
53
+ """Sanity: a bare print of non-ASCII to cp1252 DOES fail. If this ever stops
54
+ failing, this whole file is testing nothing and must be re-examined."""
55
+ r = _run(f"print({_HOSTILE!r})", "cp1252")
56
+ assert r.returncode != 0
57
+ assert "UnicodeEncodeError" in r.stderr
58
+
59
+
60
+ def test_log_dm_survives_a_cp1252_stdout(tmp_path):
61
+ """THE GUARD. The delivery path must not raise on a console that cannot represent
62
+ the DM — the audit trail is written first and the operator line degrades."""
63
+ r = _run(f"""
64
+ import sys
65
+ from swarph_cli.commands import daemon
66
+ class S:
67
+ inbox_log_path = __import__('pathlib').Path({str(tmp_path / 'inbox.log')!r})
68
+ daemon._log_dm(S(), {{'id': 1, 'created_at': 'now', 'from_node': 'lab-ovh',
69
+ 'kind': 'answer',
70
+ 'content': 'réponse — arrow → and emoji ✅'}})
71
+ print("SURVIVED")
72
+ """, "cp1252")
73
+ assert r.returncode == 0, f"delivery path died on an 8-bit console:\n{r.stderr}"
74
+ assert "SURVIVED" in r.stdout
75
+
76
+
77
+ def test_the_audit_trail_is_byte_faithful_under_cp1252(tmp_path):
78
+ """The console may mangle; the record may not. inbox.log is utf-8 explicitly."""
79
+ log = tmp_path / "inbox.log"
80
+ content = "réponse — arrow → and emoji ✅"
81
+ r = _run(f"""
82
+ from pathlib import Path
83
+ from swarph_cli.commands import daemon
84
+ class S:
85
+ inbox_log_path = Path({str(log)!r})
86
+ daemon._log_dm(S(), {{'id': 7, 'created_at': 'now', 'from_node': 'x',
87
+ 'kind': 'answer', 'content': {content!r}}})
88
+ """, "cp1252")
89
+ assert r.returncode == 0, r.stderr
90
+ import json
91
+ row = json.loads(log.read_text(encoding="utf-8").strip())
92
+ assert row["content"] == content, "the audit trail lost characters the console could not print"
@@ -0,0 +1,174 @@
1
+ """A console that cannot print a DM must not stop the mesh delivering it.
2
+
3
+ MEASURED on workstation-lc 2026-07-29 — French Windows, cp1252 console, swarph-cli
4
+ 0.39.4, Python 3.14, launched by Task Scheduler via `cmd.exe /c`:
5
+
6
+ [swarph-daemon] iteration error (continuing): UnicodeEncodeError:
7
+ 'charmap' codec can't encode character '→' in position 65
8
+
9
+ `_log_dm` printed a HARDCODED U+2192 to a charmap stdout. Every DM raised; every
10
+ `_drain_iteration` aborted; THE CURSOR FROZE AT last_msg_id=10050 AND ZERO DMS WERE
11
+ DELIVERED. The outer handler swallowed it as "iteration error (continuing)", so
12
+ Get-ScheduledTask said Running, the process list looked healthy, and every surface was
13
+ green. >>> THE CELL WAS DM-BLIND AND NOTHING ANYWHERE SAID SO. <<<
14
+
15
+ Note what was NOT at risk: inbox.log was already opened with encoding="utf-8", so the
16
+ audit trail was intact throughout. Only the OPERATOR-VISIBLE line was unprintable — and
17
+ it took the delivery loop down with it. Hence the ordering fix: audit first, display
18
+ best-effort.
19
+ """
20
+ import io
21
+ import json
22
+
23
+ import pytest
24
+
25
+ from swarph_cli.commands import daemon
26
+
27
+
28
+ class _Cp1252Stdout(io.TextIOBase):
29
+ """A console that can only encode cp1252 — a French Windows box, faithfully."""
30
+
31
+ encoding = "cp1252"
32
+
33
+ def __init__(self):
34
+ self.written = []
35
+
36
+ def write(self, s):
37
+ s.encode("cp1252") # raises UnicodeEncodeError exactly as Windows does
38
+ self.written.append(s)
39
+ return len(s)
40
+
41
+ def flush(self):
42
+ pass
43
+
44
+
45
+ class _State:
46
+ def __init__(self, tmp_path):
47
+ self.inbox_log_path = tmp_path / "inbox.log"
48
+
49
+
50
+ ARROW_DM = {"id": 1, "created_at": "2026-07-29T02:00Z", "from_node": "lab-ovh",
51
+ "kind": "answer", "content": "see -> the thing → and été"}
52
+
53
+
54
+ def test_no_hardcoded_non_ascii_in_the_operator_line():
55
+ """The separator was ours, not the DM's — so it failed on EVERY message, not only
56
+ ones containing an arrow. An ASCII separator removes the guaranteed-failure case."""
57
+ import inspect
58
+ src = inspect.getsource(daemon._log_dm)
59
+ body = src.split('"""')[-1] # skip the docstring, which describes the bug
60
+ assert "→" not in body, "operator line must not hardcode non-ASCII"
61
+
62
+
63
+ def test_cp1252_console_does_not_stop_delivery(tmp_path, monkeypatch):
64
+ """THE LOAD-BEARING TEST. A console that refuses the content must cost a mangled
65
+ character, never a poll iteration."""
66
+ fake = _Cp1252Stdout()
67
+ monkeypatch.setattr(daemon.sys, "stdout", fake)
68
+ daemon._log_dm(_State(tmp_path), ARROW_DM) # must NOT raise
69
+ assert fake.written, "should still have written something to the console"
70
+
71
+
72
+ def test_audit_trail_is_written_even_when_the_console_fails(tmp_path, monkeypatch):
73
+ """Ordering is the fix: audit FIRST, display second. Losing a console line is
74
+ cosmetic; losing the cursor is a silent outage."""
75
+ monkeypatch.setattr(daemon.sys, "stdout", _Cp1252Stdout())
76
+ st = _State(tmp_path)
77
+ daemon._log_dm(st, ARROW_DM)
78
+ rows = [json.loads(l) for l in st.inbox_log_path.read_text(encoding="utf-8").splitlines()]
79
+ assert rows and rows[0]["id"] == 1
80
+ assert rows[0]["content"] == ARROW_DM["content"], "audit must be byte-faithful"
81
+
82
+
83
+ def test_broken_stdout_does_not_stop_delivery(tmp_path, monkeypatch):
84
+ """A detached service or a full disk closes stdout. Still not a reason to stop."""
85
+ class _Broken(io.TextIOBase):
86
+ encoding = "utf-8"
87
+ def write(self, s): raise OSError("stdout is closed")
88
+ def flush(self): pass
89
+
90
+ monkeypatch.setattr(daemon.sys, "stdout", _Broken())
91
+ daemon._log_dm(_State(tmp_path), ARROW_DM) # must NOT raise
92
+ assert (tmp_path / "inbox.log").exists()
93
+
94
+
95
+ def test_utf8_console_is_unaffected(tmp_path, monkeypatch, capsys):
96
+ """The common path must not be degraded by the fallback."""
97
+ daemon._log_dm(_State(tmp_path), ARROW_DM)
98
+ out = capsys.readouterr().out
99
+ assert "id=1" in out and "lab-ovh" in out
100
+
101
+
102
+ # ── COVERAGE, not presence — droplet's finding on PR #158 ────────────────────
103
+
104
+ def test_every_output_path_in_the_daemon_goes_through_the_guard():
105
+ """>>> A SYMBOL GREP MEASURES PRESENCE, NOT COVERAGE. <<<
106
+
107
+ The first version of the cp1252 guard shipped covering 1 output call out of 13.
108
+ `grep -c _print_safe` returned 1 on it — the same answer it returns now, with all
109
+ 13 covered. Any check that asks "is the guard there?" passes on both, so this asks
110
+ the only question that distinguishes them: IS THERE AN UNGUARDED `print`?
111
+
112
+ Walks the AST rather than the text so a `print` reached through a differently
113
+ formatted call, a new function, or a future author's copy-paste is caught. Same
114
+ reason the merge-check no-merge guard is behavioural and not a substring scan:
115
+ the claim is about what the module DOES, and text is a proxy for that.
116
+ """
117
+ import ast
118
+ from pathlib import Path
119
+
120
+ import swarph_cli.commands.daemon as mod
121
+
122
+ src = Path(mod.__file__).read_text(encoding="utf-8")
123
+ tree = ast.parse(src)
124
+ guard = next(n for n in ast.walk(tree)
125
+ if isinstance(n, ast.FunctionDef) and n.name == "_print_safe")
126
+ allowed = {id(n) for n in ast.walk(guard)}
127
+
128
+ unguarded = [
129
+ n.lineno for n in ast.walk(tree)
130
+ if isinstance(n, ast.Call) and isinstance(n.func, ast.Name)
131
+ and n.func.id == "print" and id(n) not in allowed
132
+ ]
133
+ assert not unguarded, (
134
+ f"unguarded print() at daemon.py lines {unguarded} — on a cp1252 console an "
135
+ f"arrow or emoji in that line raises UnicodeEncodeError and takes the loop with it"
136
+ )
137
+
138
+
139
+ def test_the_delivery_error_handler_survives_a_character_it_cannot_render():
140
+ """THE CRASH droplet MEASURED: a raise inside an `except` propagates past the
141
+ `try` entirely, so the handler whose comment promises it never crashes the loop
142
+ was itself the crash — reached only when something has ALREADY gone wrong, i.e.
143
+ exactly when the text is most likely to carry an arrow or an emoji.
144
+
145
+ Not a test of the happy DM path: that path was already guarded, so a clean
146
+ non-ASCII DM round-trip PASSES while this crash survives. The failure path is
147
+ the one that has to be driven.
148
+ """
149
+ import io
150
+
151
+ from swarph_cli.commands import daemon as mod
152
+
153
+ class Cp1252Stream(io.StringIO):
154
+ encoding = "cp1252"
155
+
156
+ def write(self, s): # raises exactly where a French Windows console raises
157
+ s.encode("cp1252")
158
+ return super().write(s)
159
+
160
+ for payload in ("plain ascii", "arrow -> →", "emoji \U0001f512", "check ✓"):
161
+ stream = Cp1252Stream()
162
+ try:
163
+ raise RuntimeError(payload)
164
+ except RuntimeError as exc:
165
+ mod._print_safe(
166
+ f"[swarph-daemon] delivery error (continuing): "
167
+ f"{type(exc).__name__}: {exc}",
168
+ stream=stream,
169
+ ) # must not raise — that IS the bug
170
+
171
+ # canary: the model must still be able to fail, or this file asserts nothing
172
+ import pytest
173
+ with pytest.raises(UnicodeEncodeError):
174
+ Cp1252Stream().write("→")
@@ -1043,4 +1043,4 @@ def test_parser_defaults_process_name_claude_liveness_cmd_none():
1043
1043
 
1044
1044
  def test_version_is_0_33_0():
1045
1045
  import swarph_cli
1046
- assert swarph_cli.__version__ == "0.40.0"
1046
+ assert swarph_cli.__version__ == "0.40.1"
File without changes
File without changes
File without changes