swarph-cli 0.40.1__tar.gz → 0.40.3__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 (225) hide show
  1. {swarph_cli-0.40.1/src/swarph_cli.egg-info → swarph_cli-0.40.3}/PKG-INFO +1 -1
  2. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/pyproject.toml +1 -1
  3. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/__init__.py +1 -1
  4. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/daemon.py +6 -47
  5. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/onboard.py +24 -23
  6. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/schedule.py +75 -2
  7. swarph_cli-0.40.3/src/swarph_cli/console_safe.py +78 -0
  8. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/delivery_queue.py +2 -1
  9. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/gateway/server.py +9 -2
  10. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/stall_alert.py +2 -1
  11. {swarph_cli-0.40.1 → swarph_cli-0.40.3/src/swarph_cli.egg-info}/PKG-INFO +1 -1
  12. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli.egg-info/SOURCES.txt +2 -0
  13. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_brain_ask_command.py +1 -1
  14. swarph_cli-0.40.3/tests/test_daemon_console_encoding.py +273 -0
  15. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_meta_edge_identity.py +47 -0
  16. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_schedule_command.py +35 -4
  17. swarph_cli-0.40.3/tests/test_schedule_create_contract.py +154 -0
  18. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_watchdog.py +1 -1
  19. swarph_cli-0.40.1/tests/test_daemon_console_encoding.py +0 -174
  20. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/LICENSE +0 -0
  21. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/README.md +0 -0
  22. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/setup.cfg +0 -0
  23. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/bench/__init__.py +0 -0
  24. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/bench/backends.py +0 -0
  25. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/bench/pack.py +0 -0
  26. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/bench/prices.py +0 -0
  27. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/bench/quality.py +0 -0
  28. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/bench/refresh_prices.py +0 -0
  29. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/bench/runner.py +0 -0
  30. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/bench/validate.py +0 -0
  31. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/caller.py +0 -0
  32. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/capture/__init__.py +0 -0
  33. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/capture/harden.py +0 -0
  34. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/capture/lineage.py +0 -0
  35. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/capture/liveness.py +0 -0
  36. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/capture/manifest.py +0 -0
  37. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/capture/paths.py +0 -0
  38. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/capture/verify.py +0 -0
  39. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/cell.py +0 -0
  40. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/chain_token.py +0 -0
  41. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/__init__.py +0 -0
  42. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/_display.py +0 -0
  43. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/_gateway_client.py +0 -0
  44. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/add.py +0 -0
  45. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/bench.py +0 -0
  46. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/board.py +0 -0
  47. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/board_merge_check.py +0 -0
  48. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/brain.py +0 -0
  49. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/brain_ask.py +0 -0
  50. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/cell.py +0 -0
  51. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/cell_selfcheck.py +0 -0
  52. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/channel.py +0 -0
  53. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/chat.py +0 -0
  54. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/codegraph.py +0 -0
  55. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/compress.py +0 -0
  56. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/event.py +0 -0
  57. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/gateway.py +0 -0
  58. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/group.py +0 -0
  59. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/highlight.py +0 -0
  60. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/hook_output.py +0 -0
  61. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/hooks.py +0 -0
  62. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/import_session.py +0 -0
  63. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/init.py +0 -0
  64. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/install_hook.py +0 -0
  65. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/install_multiplexer.py +0 -0
  66. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/lane.py +0 -0
  67. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/mcp_server.py +0 -0
  68. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/memory.py +0 -0
  69. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/memory_sync.py +0 -0
  70. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/mesh.py +0 -0
  71. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/monitor.py +0 -0
  72. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/okf_links.py +0 -0
  73. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/protocol_handler.py +0 -0
  74. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/ratify.py +0 -0
  75. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/security.py +0 -0
  76. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/service.py +0 -0
  77. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/spawn.py +0 -0
  78. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/timeline.py +0 -0
  79. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/watchdog.py +0 -0
  80. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/compress/__init__.py +0 -0
  81. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/compress/levers.py +0 -0
  82. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/compress/marker.py +0 -0
  83. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/compress/verify.py +0 -0
  84. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/gateway/__init__.py +0 -0
  85. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/gateway/feature_registry.py +0 -0
  86. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/gateway/lanes_control.py +0 -0
  87. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/gateway/schema.sql +0 -0
  88. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/gateway/services_control.py +0 -0
  89. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/main.py +0 -0
  90. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/multiplexer.py +0 -0
  91. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/parsers/__init__.py +0 -0
  92. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/parsers/claude.py +0 -0
  93. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/scripts/ensure_monitor.sh +0 -0
  94. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/service/__init__.py +0 -0
  95. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/service/app.py +0 -0
  96. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/service/providers.py +0 -0
  97. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/session_bridge.py +0 -0
  98. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
  99. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
  100. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
  101. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
  102. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli.egg-info/entry_points.txt +0 -0
  103. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli.egg-info/requires.txt +0 -0
  104. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli.egg-info/top_level.txt +0 -0
  105. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_add_security_gate.py +0 -0
  106. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_artifact_add.py +0 -0
  107. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_artifact_hash.py +0 -0
  108. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_artifact_lib.py +0 -0
  109. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_artifact_mcp.py +0 -0
  110. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_artifact_skill.py +0 -0
  111. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_artifact_tool.py +0 -0
  112. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_artifact_uri.py +0 -0
  113. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_bench_backends.py +0 -0
  114. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_bench_command.py +0 -0
  115. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_bench_pack.py +0 -0
  116. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_bench_prices.py +0 -0
  117. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_bench_quality.py +0 -0
  118. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_bench_runner.py +0 -0
  119. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_bench_validate.py +0 -0
  120. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_board_command.py +0 -0
  121. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_board_merge_check.py +0 -0
  122. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_brain_ask_env_diagnosis.py +0 -0
  123. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_brain_command.py +0 -0
  124. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_caller_meta_guard.py +0 -0
  125. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_capture_lineage.py +0 -0
  126. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_capture_liveness.py +0 -0
  127. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_capture_manifest.py +0 -0
  128. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_capture_paths.py +0 -0
  129. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_capture_security.py +0 -0
  130. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_cell_command_dispatch.py +0 -0
  131. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_cell_harden.py +0 -0
  132. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_cell_loader.py +0 -0
  133. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_cell_selfcheck.py +0 -0
  134. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_cell_verify.py +0 -0
  135. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_chain_token.py +0 -0
  136. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_channel_command.py +0 -0
  137. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_channel_post.py +0 -0
  138. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_chat_command.py +0 -0
  139. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_claude_parser.py +0 -0
  140. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_claude_tmux_template.py +0 -0
  141. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_codegraph_command.py +0 -0
  142. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_codegraph_mcp.py +0 -0
  143. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_compress_caller_convention.py +0 -0
  144. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_compress_command.py +0 -0
  145. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_compress_levers.py +0 -0
  146. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_compress_marker.py +0 -0
  147. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_compress_verify.py +0 -0
  148. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_console_encoding_e2e.py +0 -0
  149. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_daemon_command.py +0 -0
  150. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_daemon_delivery.py +0 -0
  151. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_delivery_queue.py +0 -0
  152. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_display_sanitize.py +0 -0
  153. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_event_emit.py +0 -0
  154. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_feature_to_uri.py +0 -0
  155. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_gateway_brain.py +0 -0
  156. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_gateway_client.py +0 -0
  157. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_gateway_command.py +0 -0
  158. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_group_command.py +0 -0
  159. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_highlight_command.py +0 -0
  160. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_hook_output.py +0 -0
  161. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_hooks_activity_marker.py +0 -0
  162. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_hooks_add.py +0 -0
  163. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_hooks_bundle.py +0 -0
  164. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_hooks_init.py +0 -0
  165. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_hooks_lifecycle.py +0 -0
  166. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_hooks_merge.py +0 -0
  167. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_import_command.py +0 -0
  168. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_init_command.py +0 -0
  169. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_install_hook.py +0 -0
  170. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_install_multiplexer_command.py +0 -0
  171. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_lane_command.py +0 -0
  172. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_main.py +0 -0
  173. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_main_unknown_verb_guard.py +0 -0
  174. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_mcp_server.py +0 -0
  175. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_memory_command.py +0 -0
  176. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_memory_gateway.py +0 -0
  177. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_memory_sync.py +0 -0
  178. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_memory_sync_membrane.py +0 -0
  179. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_mesh_command.py +0 -0
  180. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_mesh_inbox_mark_read.py +0 -0
  181. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_mesh_inbox_sanitize.py +0 -0
  182. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_mesh_sidecar.py +0 -0
  183. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_mesh_sidecar_cursor_decoupled.py +0 -0
  184. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_mesh_sidecar_wake_survives_read.py +0 -0
  185. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_monitor_cli.py +0 -0
  186. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_monitor_daemon_collision.py +0 -0
  187. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_monitor_self_name_verification.py +0 -0
  188. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_monitor_state_split.py +0 -0
  189. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_multiplexer.py +0 -0
  190. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_okf_links.py +0 -0
  191. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_onboard_command.py +0 -0
  192. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_packaged_artifacts_exist.py +0 -0
  193. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_protocol_handler.py +0 -0
  194. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_providers_isolation.py +0 -0
  195. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_ratify_command.py +0 -0
  196. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_repl_caller_convention.py +0 -0
  197. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_security.py +0 -0
  198. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_service_app.py +0 -0
  199. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_service_command.py +0 -0
  200. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_service_providers.py +0 -0
  201. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_session_bridge_inject.py +0 -0
  202. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_session_bridge_probe.py +0 -0
  203. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_session_bridge_resolve.py +0 -0
  204. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_session_path_role_gate.py +0 -0
  205. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_shipped_units_match_the_cli.py +0 -0
  206. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_smoke_chat.py +0 -0
  207. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_smoke_one_shot.py +0 -0
  208. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_smoke_phase_5_5.py +0 -0
  209. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_spawn_base_hoist.py +0 -0
  210. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_spawn_command.py +0 -0
  211. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_spawn_live_pin.py +0 -0
  212. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_spawn_mitosis_lineage.py +0 -0
  213. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_spawn_tmux_session.py +0 -0
  214. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_spawn_windows_relaunch.py +0 -0
  215. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_stall_alert.py +0 -0
  216. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_timeline_command.py +0 -0
  217. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_token_file_one_parser.py +0 -0
  218. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_version_consistency.py +0 -0
  219. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_watchdog_dm_wake.py +0 -0
  220. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
  221. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_watchdog_dm_wake_wiring.py +0 -0
  222. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_watchdog_emit_health.py +0 -0
  223. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_watchdog_install_flags.py +0 -0
  224. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_watchdog_model_rung.py +0 -0
  225. {swarph_cli-0.40.1 → swarph_cli-0.40.3}/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.1
3
+ Version: 0.40.3
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.1"
7
+ version = "0.40.3"
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.1"
19
+ __version__ = "0.40.3"
20
20
 
21
21
  __all__ = ["__version__"]
@@ -50,6 +50,7 @@ from pathlib import Path
50
50
  from typing import Optional
51
51
 
52
52
  from swarph_cli import session_bridge, stall_alert
53
+ from swarph_cli.console_safe import print_safe
53
54
  from swarph_cli.delivery_queue import DeliveryQueue
54
55
 
55
56
 
@@ -294,53 +295,11 @@ def _log_dm(state: DaemonState, dm: dict) -> None:
294
295
  _print_safe(line)
295
296
 
296
297
 
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
298
+ # The guard now lives in swarph_cli.console_safe so the modules daemon IMPORTS
299
+ # (delivery_queue, stall_alert, session_bridge) can use it too siting it here
300
+ # is what left them unguarded in the published 0.40.1. Alias kept: _print_safe is
301
+ # this module's established spelling and the AST coverage test keys on it.
302
+ _print_safe = print_safe
344
303
 
345
304
 
346
305
  def _route_to_handler(state: DaemonState, dm: dict) -> None:
@@ -32,6 +32,7 @@ import urllib.request
32
32
  from getpass import getpass
33
33
  from pathlib import Path
34
34
  from typing import Optional
35
+ from swarph_cli.console_safe import print_safe
35
36
 
36
37
 
37
38
  _HANDSHAKE_TEMPLATE = """\
@@ -147,7 +148,7 @@ def _resolve_token(token_file_arg: Optional[str]) -> str:
147
148
  try:
148
149
  mode = secrets_path.stat().st_mode & 0o777
149
150
  if mode != 0o600:
150
- print(
151
+ print_safe(
151
152
  f"swarph onboard: WARNING: {secrets_path} mode is {oct(mode)}, "
152
153
  f"expected 0600. Continuing — fix manually with `chmod 600 {secrets_path}`.",
153
154
  file=sys.stderr,
@@ -162,11 +163,11 @@ def _resolve_token(token_file_arg: Optional[str]) -> str:
162
163
  if val:
163
164
  return val
164
165
  except Exception as exc:
165
- print(
166
+ print_safe(
166
167
  f"swarph onboard: failed to read {secrets_path}: {exc}", file=sys.stderr
167
168
  )
168
169
 
169
- print(
170
+ print_safe(
170
171
  f"swarph onboard: MESH_GATEWAY_TOKEN not in env, not found in {secrets_path}.\n"
171
172
  f" Canonical secrets.toml shape (mode 0600):\n"
172
173
  f" MESH_GATEWAY_TOKEN=<your-token>\n"
@@ -225,7 +226,7 @@ def run_onboard(argv: list[str]) -> int:
225
226
  args = _build_parser().parse_args(argv)
226
227
 
227
228
  # ── Step 1: validate_node_name ───────────────────────────────────
228
- print(f"[1/6] validate_node_name({args.peer!r})")
229
+ print_safe(f"[1/6] validate_node_name({args.peer!r})")
229
230
  try:
230
231
  from swarph_shared.peer_registry import (
231
232
  validate_node_name,
@@ -233,7 +234,7 @@ def run_onboard(argv: list[str]) -> int:
233
234
  GatewayUnreachableError,
234
235
  )
235
236
  except ImportError as exc:
236
- print(f"swarph onboard: missing swarph-shared>=0.2.0: {exc}", file=sys.stderr)
237
+ print_safe(f"swarph onboard: missing swarph-shared>=0.2.0: {exc}", file=sys.stderr)
237
238
  return 1
238
239
 
239
240
  # NotInRegistry is expected here — onboard's whole point is that
@@ -241,24 +242,24 @@ def run_onboard(argv: list[str]) -> int:
241
242
  try:
242
243
  from swarph_shared.peer_registry import NAMING_CONVENTION_REGEX, KNOWN_ALIASES
243
244
  except ImportError:
244
- print("swarph onboard: peer_registry primitives missing", file=sys.stderr)
245
+ print_safe("swarph onboard: peer_registry primitives missing", file=sys.stderr)
245
246
  return 1
246
247
 
247
248
  canonical = KNOWN_ALIASES.get(args.peer, args.peer)
248
249
  if canonical != args.peer:
249
- print(
250
+ print_safe(
250
251
  f" WARN: {args.peer!r} resolved to canonical {canonical!r} "
251
252
  f"(contagion alias)",
252
253
  file=sys.stderr,
253
254
  )
254
255
  if not NAMING_CONVENTION_REGEX.match(canonical):
255
- print(
256
+ print_safe(
256
257
  f"swarph onboard: {canonical!r} fails naming convention "
257
258
  f"(^[a-z][a-z0-9-]*[a-z0-9]$)",
258
259
  file=sys.stderr,
259
260
  )
260
261
  return 1
261
- print(f" ok ({canonical!r})")
262
+ print_safe(f" ok ({canonical!r})")
262
263
 
263
264
  # ── Step 2: would-write peer-registry row (effectively step 4) ───
264
265
  # The PLAN's step 2 is logically subsumed by step 4 (the gateway
@@ -267,40 +268,40 @@ def run_onboard(argv: list[str]) -> int:
267
268
  capabilities = dict(_parse_capability(c) for c in args.capability) if args.capability else {
268
269
  "can_claim_tasks": True
269
270
  }
270
- print(f"[2/6] prepare peer-registry row (caps={capabilities})")
271
+ print_safe(f"[2/6] prepare peer-registry row (caps={capabilities})")
271
272
 
272
273
  # ── Step 3: resolve MESH_GATEWAY_TOKEN ───────────────────────────
273
- print("[3/6] resolve MESH_GATEWAY_TOKEN")
274
+ print_safe("[3/6] resolve MESH_GATEWAY_TOKEN")
274
275
  token = _resolve_token(args.token_file)
275
276
  if not token:
276
- print("swarph onboard: empty token", file=sys.stderr)
277
+ print_safe("swarph onboard: empty token", file=sys.stderr)
277
278
  return 1
278
- print(" ok")
279
+ print_safe(" ok")
279
280
 
280
281
  # ── Step 4: POST /peers/register ─────────────────────────────────
281
282
  peer_url = args.url or f"http://{canonical}:8787"
282
- print(f"[4/6] POST {args.gateway}/peers/register")
283
+ print_safe(f"[4/6] POST {args.gateway}/peers/register")
283
284
  status, body = _post_json(
284
285
  f"{args.gateway}/peers/register",
285
286
  {"name": canonical, "url": peer_url, "capabilities": capabilities},
286
287
  token,
287
288
  )
288
289
  if status != 200:
289
- print(
290
+ print_safe(
290
291
  f"swarph onboard: gateway register failed: {status} {body}",
291
292
  file=sys.stderr,
292
293
  )
293
294
  return 2
294
295
  if body.get("registered_unratified") is False:
295
- print(
296
+ print_safe(
296
297
  f" ok (already ratified — peer existed pre-Phase-5.5 or was "
297
298
  f"witness-flipped already)"
298
299
  )
299
300
  else:
300
- print(f" ok (registered_unratified=true)")
301
+ print_safe(f" ok (registered_unratified=true)")
301
302
 
302
303
  # ── Step 5: subscription auth check ──────────────────────────────
303
- print("[5/6] verify_subscription_setup()")
304
+ print_safe("[5/6] verify_subscription_setup()")
304
305
  try:
305
306
  from swarph_shared import verify_subscription_setup
306
307
 
@@ -308,9 +309,9 @@ def run_onboard(argv: list[str]) -> int:
308
309
  # catch broadly so onboarding doesn't blow up on Claude-runtime-only
309
310
  # checks when the peer is non-Claude (§15.6 #10 deferred to Phase 6).
310
311
  verify_subscription_setup()
311
- print(" ok (Claude subscription credentials + binary verified)")
312
+ print_safe(" ok (Claude subscription credentials + binary verified)")
312
313
  except Exception as exc:
313
- print(
314
+ print_safe(
314
315
  f" WARN: {type(exc).__name__}: {exc}\n"
315
316
  f" Subscription path won't work for this peer until resolved. "
316
317
  f"Non-Claude runtimes (Gemini, etc.) ship in Phase 6 per §15.6 #10.",
@@ -324,7 +325,7 @@ def run_onboard(argv: list[str]) -> int:
324
325
  else Path.home() / "swarph_state"
325
326
  )
326
327
  peer_dir = state_root / canonical
327
- print(f"[6/6] scaffold {peer_dir}")
328
+ print_safe(f"[6/6] scaffold {peer_dir}")
328
329
  peer_dir.mkdir(parents=True, exist_ok=True)
329
330
  try:
330
331
  peer_dir.chmod(0o700)
@@ -361,7 +362,7 @@ def run_onboard(argv: list[str]) -> int:
361
362
  daemon_sh.chmod(0o755)
362
363
  except OSError:
363
364
  pass # best-effort; Windows or fs without POSIX modes
364
- print(f" ok (inbox.log, cursor.json, .env.example, run-daemon.sh)")
365
+ print_safe(f" ok (inbox.log, cursor.json, .env.example, run-daemon.sh)")
365
366
 
366
367
  # ── Step 7: handshake template (MANUAL) ──────────────────────────
367
368
  # tempfile.gettempdir() is the platform temp dir ('/tmp' on POSIX, the
@@ -374,7 +375,7 @@ def run_onboard(argv: list[str]) -> int:
374
375
  ),
375
376
  encoding="utf-8",
376
377
  )
377
- print(
378
+ print_safe(
378
379
  f"\n[manual] handshake template at {tmp_path}\n"
379
380
  f" Edit each section in your own words, then send to your witness peer.\n"
380
381
  f" After witness reads + judges sufficient, they run:\n"
@@ -40,7 +40,13 @@ def _build_parser() -> argparse.ArgumentParser:
40
40
  "--context",
41
41
  action="append",
42
42
  default=[],
43
- help="context reference (repeatable)",
43
+ metavar="KEY=VALUE",
44
+ help="durable context anchor, repeatable. KEY is one of "
45
+ + "|".join(sorted(DURABLE_ANCHOR_KEYS))
46
+ + " (e.g. --context memory=project_x --context repo=swarph-cli). "
47
+ "A raw JSON object is also accepted. REQUIRED: the gateway rejects an "
48
+ "event with no anchors, because an event that fires with no durable "
49
+ "context wakes a cell that cannot reconstruct why.",
44
50
  )
45
51
  create.add_argument(
46
52
  "--min-interval",
@@ -93,14 +99,81 @@ def _ctx(args: argparse.Namespace) -> tuple[str, str, str]:
93
99
  return self_name, token, base
94
100
 
95
101
 
102
+
103
+ # Mirrors mesh-gateway `_DURABLE_ANCHOR_KEYS` (server.py). Kept in sync by
104
+ # test_schedule_create_contract.py, which fails if this drifts from the value the
105
+ # gateway actually enforces — a duplicated constant nobody checks is how the two
106
+ # sides diverged in the first place.
107
+ DURABLE_ANCHOR_KEYS = frozenset({"repo", "memory", "channel", "feature", "file"})
108
+
109
+
110
+ def parse_context_anchor(raw: str) -> dict:
111
+ """Turn one --context value into the ANCHOR DICT the gateway requires.
112
+
113
+ >>> THE BUG THIS FIXES (card #146): the CLI sent `--context` values through as
114
+ RAW STRINGS while the gateway requires each anchor to be a DICT carrying a
115
+ durable key, and requires the list to be NON-EMPTY. So `swarph schedule
116
+ create` COULD NOT SUCCEED — every invocation 400'd, and every real event on
117
+ this mesh had to be armed by hand over the raw API. <<<
118
+
119
+ That is why dated reminders kept living in DM threads instead of the scheduler:
120
+ the verb that makes a date fire was itself broken, so the cheapest path was
121
+ always to tell a peer and hope.
122
+
123
+ Accepts `key=value` for the five durable kinds, or a raw JSON object for
124
+ anything the shorthand cannot express. Raises ValueError with the valid keys
125
+ listed, LOCALLY — a 400 from the gateway names the rule but not the input that
126
+ broke it, and the user is holding the input.
127
+ """
128
+ raw = (raw or "").strip()
129
+ if not raw:
130
+ raise ValueError("empty --context value")
131
+ if raw.startswith("{"):
132
+ try:
133
+ anchor = json.loads(raw)
134
+ except json.JSONDecodeError as exc:
135
+ raise ValueError(f"--context {raw!r} looks like JSON but does not parse: {exc}") from exc
136
+ if not isinstance(anchor, dict):
137
+ raise ValueError(f"--context JSON must be an object, got {type(anchor).__name__}")
138
+ if not (set(anchor) & DURABLE_ANCHOR_KEYS):
139
+ raise ValueError(
140
+ f"--context {raw!r} has no durable key; one of "
141
+ f"{sorted(DURABLE_ANCHOR_KEYS)} is required")
142
+ return anchor
143
+ key, sep, value = raw.partition("=")
144
+ key, value = key.strip(), value.strip()
145
+ if not sep or not value:
146
+ raise ValueError(
147
+ f"--context {raw!r} must be KEY=VALUE (e.g. memory=project_x); "
148
+ f"KEY is one of {sorted(DURABLE_ANCHOR_KEYS)}")
149
+ if key not in DURABLE_ANCHOR_KEYS:
150
+ raise ValueError(
151
+ f"--context key {key!r} is not durable; use one of {sorted(DURABLE_ANCHOR_KEYS)}. "
152
+ f"An anchor must name something that survives compaction — a /tmp path or a "
153
+ f"session id is exactly what the gateway rejects.")
154
+ return {key: value}
155
+
156
+
96
157
  def _run_create(args: argparse.Namespace) -> int:
97
158
  self_name, token, base = _ctx(args)
159
+ if not args.context:
160
+ print("swarph schedule create: --context is REQUIRED (repeatable).\n"
161
+ " The gateway refuses an event with no durable anchor: an event that\n"
162
+ " fires with no context wakes a cell that cannot reconstruct why it woke.\n"
163
+ f" Use KEY=VALUE with KEY in {sorted(DURABLE_ANCHOR_KEYS)},\n"
164
+ " e.g. --context memory=project_graduation_register", file=sys.stderr)
165
+ return 2
166
+ try:
167
+ anchors = [parse_context_anchor(c) for c in args.context]
168
+ except ValueError as exc:
169
+ print(f"swarph schedule create: {exc}", file=sys.stderr)
170
+ return 2
98
171
  body = {
99
172
  "name": args.name,
100
173
  "trigger_type": args.trigger,
101
174
  "target_cell": args.target,
102
175
  "task": args.task,
103
- "context_ref": args.context,
176
+ "context_ref": anchors,
104
177
  "created_by": self_name,
105
178
  }
106
179
  if args.cron is not None:
@@ -0,0 +1,78 @@
1
+ """Encoding-safe operator output — the one place the daemon's loop may print from.
2
+
3
+ WHY THIS IS ITS OWN MODULE rather than a helper inside ``commands/daemon.py``:
4
+ the modules that most need the guard are the ones ``daemon`` IMPORTS
5
+ (``delivery_queue``, ``stall_alert``, ``session_bridge``), so they cannot import it
6
+ back. The guard living in the consumer meant the producers went unguarded — and
7
+ that is exactly where the two surviving crashes were found (``delivery_queue.py:43``,
8
+ ``stall_alert.py:49``), by droplet, in the PUBLISHED 0.40.1 artifact.
9
+
10
+ >>> THE GUARD'S HOME WAS ITSELF A COVERAGE BOUNDARY. A protection sited inside one
11
+ module protects one module, however carefully that module is then audited. <<<
12
+
13
+ THE BUG IT EXISTS FOR. On a console whose encoding cannot represent a character
14
+ (cp1252 on a French Windows box, any 8-bit codepage), ``print`` raises
15
+ ``UnicodeEncodeError``. In an ``except`` block that raise propagates past the ``try``
16
+ entirely — so an error handler whose whole contract is "never crash the loop" becomes
17
+ the crash, and it does so on the text MOST likely to carry an unrepresentable
18
+ character: an interpolated exception message.
19
+
20
+ Measured cp1252 coverage of what this fleet actually emits:
21
+
22
+ ==================================== ==========
23
+ em-dash, bullet, accented e, guillemet renders
24
+ arrow, box-drawing, emoji, check-mark RAISES
25
+ ==================================== ==========
26
+
27
+ The em-dash renders, so the original DM-blindness on workstation-lc was an arrow or
28
+ an emoji — never punctuation.
29
+
30
+ A SYMBOL GREP MEASURES PRESENCE, NOT COVERAGE: ``grep -c _print_safe`` returns 1
31
+ whether this guards one call site or every one. Coverage is asserted by an AST walk
32
+ over every module the daemon's loop can reach (``test_daemon_console_encoding.py``),
33
+ and that test is itself proven non-vacuous by reintroducing a bare ``print``.
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ import sys
39
+
40
+ __all__ = ["print_safe"]
41
+
42
+
43
+ def print_safe(line: str, *, stream=None, file=None, flush: bool = True) -> None:
44
+ """Print, degrading to a lossy transliteration rather than raising.
45
+
46
+ A console that cannot represent the text must cost a mangled character, never a
47
+ poll iteration. Callers that also persist the line (the daemon's inbox audit)
48
+ write to disk in utf-8 BEFORE calling here, so a cp1252 box loses glyphs from its
49
+ screen and never messages from its inbox.
50
+
51
+ ``file=`` is accepted as an alias for ``stream=``, and ``flush`` is accepted and
52
+ ignored (this always flushes). Deliberate rather than sloppy: every call site was
53
+ once a ``print``, and a future author reaching for the print spelling must land in
54
+ the guard rather than a ``TypeError`` — a ``TypeError`` inside the delivery-error
55
+ handler is the same crash this function exists to prevent, wearing another name.
56
+
57
+ Passing both ``stream=`` and ``file=`` with different targets silently uses
58
+ ``stream``. Recorded decision, not an accident (droplet, PR #158): raising on the
59
+ nonsensical call would put a new ``raise`` inside the one function whose entire
60
+ contract is that it never raises — the cure would be the disease.
61
+ """
62
+ out = stream if stream is not None else (file if file is not None else sys.stdout)
63
+ try:
64
+ print(line, file=out, flush=True)
65
+ except UnicodeEncodeError:
66
+ enc = getattr(out, "encoding", None) or "ascii"
67
+ try:
68
+ out.write(line.encode(enc, errors="replace").decode(enc, errors="replace"))
69
+ out.write("\n")
70
+ out.flush()
71
+ except (OSError, UnicodeEncodeError, ValueError, AttributeError):
72
+ # The fallback failing must not resurrect the crash it exists to prevent.
73
+ # Losing an operator line is survivable; losing the loop is not.
74
+ pass
75
+ except (OSError, ValueError):
76
+ # A closed/broken stream (detached service, full disk) is no reason to stop
77
+ # draining. ValueError covers write-on-closed-file, which teardown reaches.
78
+ pass
@@ -9,6 +9,7 @@ import os
9
9
  import sys
10
10
  from pathlib import Path
11
11
  from typing import List
12
+ from swarph_cli.console_safe import print_safe
12
13
 
13
14
 
14
15
  def wake_for(kind: str, thread_id) -> bool:
@@ -40,7 +41,7 @@ class DeliveryQueue:
40
41
  # Corruption: reset to empty but LOG it — since the cursor advanced
41
42
  # on drain, wiped entries won't be re-fetched, so a silent reset
42
43
  # would lose DMs from the session (spec: "treat as empty, log, continue").
43
- print(f"[swarph-daemon] delivery queue unreadable at {self.path} "
44
+ print_safe(f"[swarph-daemon] delivery queue unreadable at {self.path} "
44
45
  f"({type(exc).__name__}: {exc}); starting empty — any queued "
45
46
  f"DMs survive only in inbox.log", file=sys.stderr, flush=True)
46
47
  self._pending = []
@@ -2437,8 +2437,15 @@ def _is_operator(auth: AuthContext) -> bool:
2437
2437
  membership/broadcast authority of the FUTURE POST surface is likewise REAL
2438
2438
  only post-C5 (a cell can already create an announce channel + own
2439
2439
  allow_broadcast=1 on it). Mitigation now: pre-seed the reserved names via the
2440
- operator at deploy so the PK-409 protects the canonical names meanwhile."""
2441
- return auth.peer is None
2440
+ operator at deploy so the PK-409 protects the canonical names meanwhile.
2441
+
2442
+ META-EDGE (B1/B2): a verified SSO login (kind='user_identity') and a
2443
+ cell-join key (kind='cell_join') BOTH carry peer=None, so a bare
2444
+ `peer is None` check made every OAuth login a full operator — an
2445
+ OAuth-driven operator factory. The genuine operator is the shared/commander
2446
+ token ALONE, which is the only context with BOTH peer=None AND kind=None.
2447
+ A Meta-Edge context is owner-scoped, never operator."""
2448
+ return auth.peer is None and auth.kind is None
2442
2449
 
2443
2450
 
2444
2451
  class ChannelCreateRequest(BaseModel):
@@ -8,6 +8,7 @@ import json
8
8
  import sys
9
9
  import urllib.error
10
10
  import urllib.request
11
+ from swarph_cli.console_safe import print_safe
11
12
 
12
13
  _STALL_FIRST = 6 # first alert after this many consecutive deferred ticks
13
14
 
@@ -46,6 +47,6 @@ def send_stall_alert(gateway: str, token: str, self_name: str,
46
47
  with urllib.request.urlopen(req, timeout=10) as resp:
47
48
  return 200 <= resp.status < 300
48
49
  except (urllib.error.URLError, OSError, ValueError) as exc:
49
- print(f"[swarph-daemon] stall-alert POST failed: {exc}",
50
+ print_safe(f"[swarph-daemon] stall-alert POST failed: {exc}",
50
51
  file=sys.stderr, flush=True)
51
52
  return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swarph-cli
3
- Version: 0.40.1
3
+ Version: 0.40.3
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
@@ -5,6 +5,7 @@ src/swarph_cli/__init__.py
5
5
  src/swarph_cli/caller.py
6
6
  src/swarph_cli/cell.py
7
7
  src/swarph_cli/chain_token.py
8
+ src/swarph_cli/console_safe.py
8
9
  src/swarph_cli/delivery_queue.py
9
10
  src/swarph_cli/main.py
10
11
  src/swarph_cli/multiplexer.py
@@ -188,6 +189,7 @@ tests/test_providers_isolation.py
188
189
  tests/test_ratify_command.py
189
190
  tests/test_repl_caller_convention.py
190
191
  tests/test_schedule_command.py
192
+ tests/test_schedule_create_contract.py
191
193
  tests/test_security.py
192
194
  tests/test_service_app.py
193
195
  tests/test_service_command.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.1"
20
+ assert swarph_cli.__version__ == "0.40.3"
21
21
 
22
22
 
23
23
  # --- request / response plumbing -------------------------------------------