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.
- {swarph_cli-0.40.1/src/swarph_cli.egg-info → swarph_cli-0.40.3}/PKG-INFO +1 -1
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/pyproject.toml +1 -1
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/__init__.py +1 -1
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/daemon.py +6 -47
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/onboard.py +24 -23
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/schedule.py +75 -2
- swarph_cli-0.40.3/src/swarph_cli/console_safe.py +78 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/delivery_queue.py +2 -1
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/gateway/server.py +9 -2
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/stall_alert.py +2 -1
- {swarph_cli-0.40.1 → swarph_cli-0.40.3/src/swarph_cli.egg-info}/PKG-INFO +1 -1
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli.egg-info/SOURCES.txt +2 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_brain_ask_command.py +1 -1
- swarph_cli-0.40.3/tests/test_daemon_console_encoding.py +273 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_meta_edge_identity.py +47 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_schedule_command.py +35 -4
- swarph_cli-0.40.3/tests/test_schedule_create_contract.py +154 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_watchdog.py +1 -1
- swarph_cli-0.40.1/tests/test_daemon_console_encoding.py +0 -174
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/LICENSE +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/README.md +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/setup.cfg +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/bench/__init__.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/bench/backends.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/bench/pack.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/bench/prices.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/bench/quality.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/bench/refresh_prices.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/bench/runner.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/bench/validate.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/caller.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/capture/__init__.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/capture/harden.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/capture/lineage.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/capture/liveness.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/capture/manifest.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/capture/paths.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/capture/verify.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/cell.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/chain_token.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/__init__.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/_display.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/_gateway_client.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/add.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/bench.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/board.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/board_merge_check.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/brain.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/brain_ask.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/cell.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/cell_selfcheck.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/channel.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/chat.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/codegraph.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/compress.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/event.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/gateway.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/group.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/highlight.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/hook_output.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/hooks.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/import_session.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/init.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/install_hook.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/install_multiplexer.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/lane.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/mcp_server.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/memory.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/memory_sync.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/mesh.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/monitor.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/okf_links.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/protocol_handler.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/ratify.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/security.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/service.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/spawn.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/timeline.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/commands/watchdog.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/compress/__init__.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/compress/levers.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/compress/marker.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/compress/verify.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/gateway/__init__.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/gateway/feature_registry.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/gateway/lanes_control.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/gateway/schema.sql +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/gateway/services_control.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/main.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/multiplexer.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/parsers/__init__.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/parsers/claude.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/scripts/ensure_monitor.sh +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/service/__init__.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/service/app.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/service/providers.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/session_bridge.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli.egg-info/entry_points.txt +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli.egg-info/requires.txt +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/src/swarph_cli.egg-info/top_level.txt +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_add_security_gate.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_artifact_add.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_artifact_hash.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_artifact_lib.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_artifact_mcp.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_artifact_skill.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_artifact_tool.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_artifact_uri.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_bench_backends.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_bench_command.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_bench_pack.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_bench_prices.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_bench_quality.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_bench_runner.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_bench_validate.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_board_command.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_board_merge_check.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_brain_ask_env_diagnosis.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_brain_command.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_caller_meta_guard.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_capture_lineage.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_capture_liveness.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_capture_manifest.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_capture_paths.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_capture_security.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_cell_command_dispatch.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_cell_harden.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_cell_loader.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_cell_selfcheck.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_cell_verify.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_chain_token.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_channel_command.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_channel_post.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_chat_command.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_claude_parser.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_claude_tmux_template.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_codegraph_command.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_codegraph_mcp.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_compress_caller_convention.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_compress_command.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_compress_levers.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_compress_marker.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_compress_verify.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_console_encoding_e2e.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_daemon_command.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_daemon_delivery.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_delivery_queue.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_display_sanitize.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_event_emit.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_feature_to_uri.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_gateway_brain.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_gateway_client.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_gateway_command.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_group_command.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_highlight_command.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_hook_output.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_hooks_activity_marker.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_hooks_add.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_hooks_bundle.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_hooks_init.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_hooks_lifecycle.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_hooks_merge.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_import_command.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_init_command.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_install_hook.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_install_multiplexer_command.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_lane_command.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_main.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_main_unknown_verb_guard.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_mcp_server.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_memory_command.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_memory_gateway.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_memory_sync.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_memory_sync_membrane.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_mesh_command.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_mesh_inbox_mark_read.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_mesh_inbox_sanitize.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_mesh_sidecar.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_mesh_sidecar_cursor_decoupled.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_mesh_sidecar_wake_survives_read.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_monitor_cli.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_monitor_daemon_collision.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_monitor_self_name_verification.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_monitor_state_split.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_multiplexer.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_okf_links.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_onboard_command.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_packaged_artifacts_exist.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_protocol_handler.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_providers_isolation.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_ratify_command.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_repl_caller_convention.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_security.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_service_app.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_service_command.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_service_providers.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_session_bridge_inject.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_session_bridge_probe.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_session_bridge_resolve.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_session_path_role_gate.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_shipped_units_match_the_cli.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_smoke_chat.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_smoke_one_shot.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_smoke_phase_5_5.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_spawn_base_hoist.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_spawn_command.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_spawn_live_pin.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_spawn_mitosis_lineage.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_spawn_tmux_session.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_spawn_windows_relaunch.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_stall_alert.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_timeline_command.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_token_file_one_parser.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_version_consistency.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_watchdog_dm_wake.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_watchdog_dm_wake_wiring.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_watchdog_emit_health.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_watchdog_install_flags.py +0 -0
- {swarph_cli-0.40.1 → swarph_cli-0.40.3}/tests/test_watchdog_model_rung.py +0 -0
- {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.
|
|
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.
|
|
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" }
|
|
@@ -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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
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
|
-
|
|
166
|
+
print_safe(
|
|
166
167
|
f"swarph onboard: failed to read {secrets_path}: {exc}", file=sys.stderr
|
|
167
168
|
)
|
|
168
169
|
|
|
169
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
271
|
+
print_safe(f"[2/6] prepare peer-registry row (caps={capabilities})")
|
|
271
272
|
|
|
272
273
|
# ── Step 3: resolve MESH_GATEWAY_TOKEN ───────────────────────────
|
|
273
|
-
|
|
274
|
+
print_safe("[3/6] resolve MESH_GATEWAY_TOKEN")
|
|
274
275
|
token = _resolve_token(args.token_file)
|
|
275
276
|
if not token:
|
|
276
|
-
|
|
277
|
+
print_safe("swarph onboard: empty token", file=sys.stderr)
|
|
277
278
|
return 1
|
|
278
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
301
|
+
print_safe(f" ok (registered_unratified=true)")
|
|
301
302
|
|
|
302
303
|
# ── Step 5: subscription auth check ──────────────────────────────
|
|
303
|
-
|
|
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
|
-
|
|
312
|
+
print_safe(" ok (Claude subscription credentials + binary verified)")
|
|
312
313
|
except Exception as exc:
|
|
313
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
20
|
+
assert swarph_cli.__version__ == "0.40.3"
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
# --- request / response plumbing -------------------------------------------
|