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.
- {swarph_cli-0.40.0/src/swarph_cli.egg-info → swarph_cli-0.40.1}/PKG-INFO +1 -1
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/pyproject.toml +1 -1
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/__init__.py +1 -1
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/daemon.py +89 -18
- {swarph_cli-0.40.0 → swarph_cli-0.40.1/src/swarph_cli.egg-info}/PKG-INFO +1 -1
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli.egg-info/SOURCES.txt +2 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_brain_ask_command.py +1 -1
- swarph_cli-0.40.1/tests/test_console_encoding_e2e.py +92 -0
- swarph_cli-0.40.1/tests/test_daemon_console_encoding.py +174 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_watchdog.py +1 -1
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/LICENSE +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/README.md +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/setup.cfg +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/bench/__init__.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/bench/backends.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/bench/pack.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/bench/prices.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/bench/quality.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/bench/refresh_prices.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/bench/runner.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/bench/validate.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/caller.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/capture/__init__.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/capture/harden.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/capture/lineage.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/capture/liveness.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/capture/manifest.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/capture/paths.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/capture/verify.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/cell.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/chain_token.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/__init__.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/_display.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/_gateway_client.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/add.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/bench.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/board.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/board_merge_check.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/brain.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/brain_ask.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/cell.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/cell_selfcheck.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/channel.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/chat.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/codegraph.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/compress.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/event.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/gateway.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/group.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/highlight.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/hook_output.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/hooks.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/import_session.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/init.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/install_hook.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/install_multiplexer.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/lane.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/mcp_server.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/memory.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/memory_sync.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/mesh.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/monitor.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/okf_links.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/onboard.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/protocol_handler.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/ratify.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/schedule.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/security.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/service.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/spawn.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/timeline.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/commands/watchdog.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/compress/__init__.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/compress/levers.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/compress/marker.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/compress/verify.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/delivery_queue.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/gateway/__init__.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/gateway/feature_registry.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/gateway/lanes_control.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/gateway/schema.sql +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/gateway/server.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/gateway/services_control.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/main.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/multiplexer.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/parsers/__init__.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/parsers/claude.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/scripts/ensure_monitor.sh +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/service/__init__.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/service/app.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/service/providers.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/session_bridge.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/stall_alert.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli.egg-info/entry_points.txt +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli.egg-info/requires.txt +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/src/swarph_cli.egg-info/top_level.txt +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_add_security_gate.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_artifact_add.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_artifact_hash.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_artifact_lib.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_artifact_mcp.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_artifact_skill.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_artifact_tool.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_artifact_uri.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_bench_backends.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_bench_command.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_bench_pack.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_bench_prices.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_bench_quality.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_bench_runner.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_bench_validate.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_board_command.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_board_merge_check.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_brain_ask_env_diagnosis.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_brain_command.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_caller_meta_guard.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_capture_lineage.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_capture_liveness.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_capture_manifest.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_capture_paths.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_capture_security.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_cell_command_dispatch.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_cell_harden.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_cell_loader.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_cell_selfcheck.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_cell_verify.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_chain_token.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_channel_command.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_channel_post.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_chat_command.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_claude_parser.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_claude_tmux_template.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_codegraph_command.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_codegraph_mcp.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_compress_caller_convention.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_compress_command.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_compress_levers.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_compress_marker.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_compress_verify.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_daemon_command.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_daemon_delivery.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_delivery_queue.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_display_sanitize.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_event_emit.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_feature_to_uri.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_gateway_brain.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_gateway_client.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_gateway_command.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_group_command.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_highlight_command.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_hook_output.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_hooks_activity_marker.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_hooks_add.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_hooks_bundle.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_hooks_init.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_hooks_lifecycle.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_hooks_merge.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_import_command.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_init_command.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_install_hook.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_install_multiplexer_command.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_lane_command.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_main.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_main_unknown_verb_guard.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_mcp_server.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_memory_command.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_memory_gateway.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_memory_sync.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_memory_sync_membrane.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_mesh_command.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_mesh_inbox_mark_read.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_mesh_inbox_sanitize.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_mesh_sidecar.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_mesh_sidecar_cursor_decoupled.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_mesh_sidecar_wake_survives_read.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_meta_edge_identity.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_monitor_cli.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_monitor_daemon_collision.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_monitor_self_name_verification.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_monitor_state_split.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_multiplexer.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_okf_links.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_onboard_command.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_packaged_artifacts_exist.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_protocol_handler.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_providers_isolation.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_ratify_command.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_repl_caller_convention.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_schedule_command.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_security.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_service_app.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_service_command.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_service_providers.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_session_bridge_inject.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_session_bridge_probe.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_session_bridge_resolve.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_session_path_role_gate.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_shipped_units_match_the_cli.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_smoke_chat.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_smoke_one_shot.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_smoke_phase_5_5.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_spawn_base_hoist.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_spawn_command.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_spawn_live_pin.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_spawn_mitosis_lineage.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_spawn_tmux_session.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_spawn_windows_relaunch.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_stall_alert.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_timeline_command.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_token_file_one_parser.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_version_consistency.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_watchdog_dm_wake.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_watchdog_dm_wake_wiring.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_watchdog_emit_health.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_watchdog_install_flags.py +0 -0
- {swarph_cli-0.40.0 → swarph_cli-0.40.1}/tests/test_watchdog_model_rung.py +0 -0
- {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.
|
|
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.
|
|
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" }
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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"
|
|
292
|
+
f"-> {dm['content'][:120]!r}"
|
|
268
293
|
)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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("→")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|