swarph-cli 0.41.2__tar.gz → 0.41.4__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.41.2/src/swarph_cli.egg-info → swarph_cli-0.41.4}/PKG-INFO +1 -1
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/pyproject.toml +1 -1
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/__init__.py +1 -1
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/board.py +57 -6
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/board_merge_check.py +32 -6
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/daemon.py +15 -1
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/onboard.py +44 -7
- {swarph_cli-0.41.2 → swarph_cli-0.41.4/src/swarph_cli.egg-info}/PKG-INFO +1 -1
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli.egg-info/SOURCES.txt +2 -0
- swarph_cli-0.41.4/tests/test_243_onboard_per_peer_token.py +86 -0
- swarph_cli-0.41.4/tests/test_84_cli_labels.py +81 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_board_merge_check.py +47 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_brain_ask_command.py +1 -1
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_onboard_command.py +23 -7
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_watchdog.py +1 -1
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/LICENSE +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/README.md +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/setup.cfg +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/bench/__init__.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/bench/backends.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/bench/pack.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/bench/prices.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/bench/quality.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/bench/refresh_prices.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/bench/runner.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/bench/validate.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/caller.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/capture/__init__.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/capture/harden.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/capture/lineage.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/capture/liveness.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/capture/manifest.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/capture/paths.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/capture/verify.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/cell.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/chain_token.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/__init__.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/_display.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/_gateway_client.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/add.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/bench.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/brain.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/brain_ask.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/cell.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/cell_selfcheck.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/channel.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/chat.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/codegraph.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/codegraph_hook.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/compress.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/event.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/gateway.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/group.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/highlight.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/hook_output.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/hooks.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/import_session.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/init.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/install_hook.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/install_multiplexer.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/lane.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/mcp_server.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/memory.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/memory_sync.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/mesh.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/monitor.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/okf_links.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/protocol_handler.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/ratify.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/schedule.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/security.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/service.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/spawn.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/timeline.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/watchdog.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/compress/__init__.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/compress/levers.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/compress/marker.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/compress/verify.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/console_safe.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/delivery_queue.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/gateway/__init__.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/gateway/feature_registry.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/gateway/lanes_control.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/gateway/schema.sql +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/gateway/server.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/gateway/services_control.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/main.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/multiplexer.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/parsers/__init__.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/parsers/claude.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/scripts/ensure_monitor.sh +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/service/__init__.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/service/app.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/service/providers.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/session_bridge.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/stall_alert.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli.egg-info/entry_points.txt +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli.egg-info/requires.txt +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli.egg-info/top_level.txt +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_add_security_gate.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_artifact_add.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_artifact_hash.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_artifact_lib.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_artifact_mcp.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_artifact_skill.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_artifact_tool.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_artifact_uri.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_bench_backends.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_bench_command.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_bench_pack.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_bench_prices.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_bench_quality.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_bench_runner.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_bench_validate.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_board_command.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_brain_ask_env_diagnosis.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_brain_command.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_caller_meta_guard.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_capture_lineage.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_capture_liveness.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_capture_manifest.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_capture_paths.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_capture_security.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_cell_command_dispatch.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_cell_harden.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_cell_loader.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_cell_selfcheck.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_cell_verify.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_chain_token.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_channel_command.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_channel_post.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_chat_command.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_claude_parser.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_claude_tmux_template.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_codegraph_caller_scope.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_codegraph_command.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_codegraph_hook.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_codegraph_mcp.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_compress_caller_convention.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_compress_command.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_compress_levers.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_compress_marker.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_compress_verify.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_console_encoding_e2e.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_daemon_command.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_daemon_console_encoding.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_daemon_delivery.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_delivery_queue.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_display_sanitize.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_event_emit.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_feature_to_uri.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_gateway_brain.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_gateway_client.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_gateway_command.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_group_command.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_highlight_command.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_hook_output.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_hooks_activity_marker.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_hooks_add.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_hooks_bundle.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_hooks_init.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_hooks_lifecycle.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_hooks_merge.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_import_command.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_init_command.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_install_hook.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_install_multiplexer_command.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_lane_command.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_main.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_main_unknown_verb_guard.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_mcp_server.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_memory_command.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_memory_gateway.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_memory_sync.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_memory_sync_membrane.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_mesh_command.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_mesh_inbox_mark_read.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_mesh_inbox_sanitize.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_mesh_sidecar.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_mesh_sidecar_cursor_decoupled.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_mesh_sidecar_wake_survives_read.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_meta_edge_identity.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_monitor_cli.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_monitor_daemon_collision.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_monitor_self_name_verification.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_monitor_state_split.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_multiplexer.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_okf_links.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_packaged_artifacts_exist.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_process_liveness.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_protocol_handler.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_providers_isolation.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_ratify_command.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_repl_caller_convention.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_schedule_command.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_schedule_create_contract.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_security.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_service_app.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_service_command.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_service_providers.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_session_bridge_inject.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_session_bridge_probe.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_session_bridge_resolve.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_session_path_role_gate.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_shipped_units_match_the_cli.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_smoke_chat.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_smoke_one_shot.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_smoke_phase_5_5.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_spawn_base_hoist.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_spawn_command.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_spawn_live_pin.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_spawn_mitosis_lineage.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_spawn_tmux_session.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_spawn_windows_relaunch.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_stall_alert.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_timeline_command.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_token_file_one_parser.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_version_consistency.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_watchdog_dm_wake.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_watchdog_dm_wake_wiring.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_watchdog_emit_health.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_watchdog_install_flags.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_watchdog_model_rung.py +0 -0
- {swarph_cli-0.41.2 → swarph_cli-0.41.4}/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.41.
|
|
3
|
+
Version: 0.41.4
|
|
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.41.
|
|
7
|
+
version = "0.41.4"
|
|
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,20 +50,43 @@ def _patch_json(url: str, body: dict, token: str, *, timeout: float = 10.0) -> t
|
|
|
50
50
|
|
|
51
51
|
# ── pure builders (unit-tested) ───────────────────────────────────────────────
|
|
52
52
|
|
|
53
|
-
def _cards_list_url(gateway: str, *, project=None, stage=None, assignee=None
|
|
54
|
-
|
|
53
|
+
def _cards_list_url(gateway: str, *, project=None, stage=None, assignee=None,
|
|
54
|
+
label=None) -> str:
|
|
55
|
+
q = {k: v for k, v in (("project", project), ("stage", stage),
|
|
56
|
+
("assignee", assignee), ("label", label)) if v}
|
|
55
57
|
base = f"{gateway.rstrip('/')}/board/cards"
|
|
56
58
|
return f"{base}?{urllib.parse.urlencode(q)}" if q else base
|
|
57
59
|
|
|
58
60
|
|
|
59
|
-
def _card_add_payload(actor, project_id, title, *, body=None, ai2=False, priority=0
|
|
61
|
+
def _card_add_payload(actor, project_id, title, *, body=None, ai2=False, priority=0,
|
|
62
|
+
labels=None) -> dict:
|
|
60
63
|
p = {"actor": actor, "project_id": int(project_id), "title": title,
|
|
61
64
|
"ai2": bool(ai2), "priority": int(priority)}
|
|
62
65
|
if body:
|
|
63
66
|
p["body"] = body
|
|
67
|
+
if labels:
|
|
68
|
+
p["labels"] = list(labels)
|
|
64
69
|
return p
|
|
65
70
|
|
|
66
71
|
|
|
72
|
+
def _apply_label(current: list, action: str, label: str) -> list:
|
|
73
|
+
"""Compute the NEW label set. Pure, so the read-modify-write is testable
|
|
74
|
+
without a gateway.
|
|
75
|
+
|
|
76
|
+
The server takes a FULL REPLACEMENT (labels are a set — a merge-only field
|
|
77
|
+
could never REMOVE one), so `label rm` is unavoidably read-modify-write. That
|
|
78
|
+
is last-writer-wins between two simultaneous editors of the SAME card, which
|
|
79
|
+
the board already has for `stage` and `assignee` — stated rather than hidden,
|
|
80
|
+
because the alternative (server-side add/rm ops) is a bigger surface than
|
|
81
|
+
this card justifies.
|
|
82
|
+
"""
|
|
83
|
+
cur = [str(x).strip().lower() for x in (current or [])]
|
|
84
|
+
lab = label.strip().lower()
|
|
85
|
+
if action == "add":
|
|
86
|
+
return cur if lab in cur else cur + [lab]
|
|
87
|
+
return [x for x in cur if x != lab]
|
|
88
|
+
|
|
89
|
+
|
|
67
90
|
def _project_add_payload(actor, slug, title, *, goal=None) -> dict:
|
|
68
91
|
p = {"actor": actor, "slug": slug, "title": title}
|
|
69
92
|
if goal:
|
|
@@ -153,13 +176,17 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
153
176
|
cards = top.add_parser("cards", help="board cards").add_subparsers(dest="command", required=True)
|
|
154
177
|
cl = cards.add_parser("list", help="list cards")
|
|
155
178
|
cl.add_argument("--project"); cl.add_argument("--stage"); cl.add_argument("--assignee")
|
|
179
|
+
cl.add_argument("--label", help="only cards carrying this label (exact match)")
|
|
156
180
|
cl.add_argument("--json", action="store_true"); _add_common(cl)
|
|
157
181
|
cs = cards.add_parser("show", help="show one card"); cs.add_argument("id", type=int)
|
|
158
182
|
cs.add_argument("--json", action="store_true"); _add_common(cs)
|
|
159
183
|
ca = cards.add_parser("add", help="create a card")
|
|
160
184
|
ca.add_argument("--project", required=True, help="project id or slug"); ca.add_argument("--title", required=True)
|
|
161
185
|
ca.add_argument("--body"); ca.add_argument("--ai2", action="store_true")
|
|
162
|
-
ca.add_argument("--priority", type=int, default=0)
|
|
186
|
+
ca.add_argument("--priority", type=int, default=0)
|
|
187
|
+
ca.add_argument("--label", action="append", dest="labels", metavar="LABEL",
|
|
188
|
+
help="attach a label (repeatable)")
|
|
189
|
+
ca.add_argument("--json", action="store_true"); _add_common(ca)
|
|
163
190
|
cm = cards.add_parser("move", help="move a card to a stage")
|
|
164
191
|
cm.add_argument("id", type=int); cm.add_argument("stage"); cm.add_argument("--json", action="store_true"); _add_common(cm)
|
|
165
192
|
ck = cards.add_parser("link", help="add/update a link on a card (merges)")
|
|
@@ -167,6 +194,10 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
167
194
|
ck.add_argument("--json", action="store_true"); _add_common(ck)
|
|
168
195
|
cn = cards.add_parser("assign", help="set a card's assignee")
|
|
169
196
|
cn.add_argument("id", type=int); cn.add_argument("assignee"); cn.add_argument("--json", action="store_true"); _add_common(cn)
|
|
197
|
+
cb = cards.add_parser("label", help="add/remove a label on a card")
|
|
198
|
+
cb.add_argument("action", choices=["add", "rm"])
|
|
199
|
+
cb.add_argument("id", type=int); cb.add_argument("label")
|
|
200
|
+
cb.add_argument("--json", action="store_true"); _add_common(cb)
|
|
170
201
|
cr = cards.add_parser("ready", help="flag a card ready-to-advance (move_ready) for the orchestrator")
|
|
171
202
|
cr.add_argument("id", type=int); cr.add_argument("--clear", action="store_true", help="unset move_ready")
|
|
172
203
|
cr.add_argument("--json", action="store_true"); _add_common(cr)
|
|
@@ -224,7 +255,9 @@ def run_board(argv: list[str]) -> int:
|
|
|
224
255
|
if err:
|
|
225
256
|
print(f"swarph board: {err}", file=sys.stderr)
|
|
226
257
|
return 1
|
|
227
|
-
st, d = _http_get_json(_cards_list_url(gw, project=pid, stage=args.stage,
|
|
258
|
+
st, d = _http_get_json(_cards_list_url(gw, project=pid, stage=args.stage,
|
|
259
|
+
assignee=args.assignee,
|
|
260
|
+
label=getattr(args, "label", None)), token)
|
|
228
261
|
return _out(st, d, _format_cards, aj)
|
|
229
262
|
if args.command == "show":
|
|
230
263
|
st, d = _http_get_json(f"{gw}/board/cards/{args.id}", token)
|
|
@@ -234,8 +267,26 @@ def run_board(argv: list[str]) -> int:
|
|
|
234
267
|
if err or pid is None:
|
|
235
268
|
print(f"swarph board: {err or 'project required'}", file=sys.stderr)
|
|
236
269
|
return 1
|
|
237
|
-
st, d = _post_json(f"{gw}/board/cards", _card_add_payload(
|
|
270
|
+
st, d = _post_json(f"{gw}/board/cards", _card_add_payload(
|
|
271
|
+
self_name, pid, args.title, body=args.body, ai2=args.ai2,
|
|
272
|
+
priority=args.priority, labels=getattr(args, "labels", None)), token)
|
|
238
273
|
return _out(st, d, lambda x: f"created card #{x.get('id')} [{x.get('stage')}] (stage defaults to proposed — use `cards move` to advance)", aj)
|
|
274
|
+
if args.command == "label":
|
|
275
|
+
# READ the current set before replacing it. If the GET fails we must
|
|
276
|
+
# NOT write — a PATCH built on a failed read would silently CLOBBER
|
|
277
|
+
# every other label on the card. Refuse loudly instead.
|
|
278
|
+
st, cur = _http_get_json(f"{gw}/board/cards/{args.id}", token)
|
|
279
|
+
if st != 200 or not isinstance(cur, dict):
|
|
280
|
+
print(f"swarph board: cannot read card #{args.id} to modify its "
|
|
281
|
+
f"labels (HTTP {st}) — refusing to write, a replacement built "
|
|
282
|
+
f"on a failed read would drop the card's other labels",
|
|
283
|
+
file=sys.stderr)
|
|
284
|
+
return 1
|
|
285
|
+
new = _apply_label(cur.get("labels") or [], args.action, args.label)
|
|
286
|
+
st, d = _patch_json(f"{gw}/board/cards/{args.id}",
|
|
287
|
+
{"actor": self_name, "labels": new}, token)
|
|
288
|
+
return _out(st, d, lambda x: f"card #{x.get('id')} labels -> "
|
|
289
|
+
f"{', '.join(x.get('labels') or []) or '(none)'}", aj)
|
|
239
290
|
if args.command == "move":
|
|
240
291
|
st, d = _patch_json(f"{gw}/board/cards/{args.id}", {"actor": self_name, "stage": args.stage}, token)
|
|
241
292
|
return _out(st, d, lambda x: f"card #{x.get('id')} -> {x.get('stage')}", aj)
|
|
@@ -161,15 +161,41 @@ def decide(card: dict, pr_state: dict) -> Decision:
|
|
|
161
161
|
# vacuous for exactly the backlog it must cover. ok=None => COULD NOT
|
|
162
162
|
# EVALUATE => REFUSE, which is this file's existing doctrine for the
|
|
163
163
|
# unreachable-GitHub case and the reason it has a tri-state at all.
|
|
164
|
+
# >>> A STAMP THAT EXISTS AND LACKS THE KEY IS AS BLIND AS NO STAMP AT ALL,
|
|
165
|
+
# AND IT IS 92% OF PRODUCTION. <<< (droplet, running decide() on the
|
|
166
|
+
# INSTALLED 0.41.2 rather than reading it.)
|
|
167
|
+
#
|
|
168
|
+
# The first version tested `_vstamp is None` — ABSENCE OF THE STAMP. The
|
|
169
|
+
# backlog's actual shape is a stamp that EXISTS (written before
|
|
170
|
+
# self_authored shipped) and lacks the KEY. That fell through to the
|
|
171
|
+
# comparison, where `.get("self_authored")` returns None, `not None` is
|
|
172
|
+
# True, and the leg reported ok=True. A textbook self-review PASSED.
|
|
173
|
+
#
|
|
174
|
+
# MEASURED: 53 link_stamps in production, 4 carrying the flag — 49 (92%)
|
|
175
|
+
# are the middle shape. So the leg was VACUOUS FOR EXACTLY THE BACKLOG IT
|
|
176
|
+
# WAS BUILT FOR: the outcome the design explicitly targets, and which I
|
|
177
|
+
# reported to the date-holder as avoided. He authorised near-total
|
|
178
|
+
# ABSTENTION on the backlog; what shipped was near-total PASS — the
|
|
179
|
+
# opposite, and MORE PERMISSIVE THAN THE PHASE-1 STATE IT REPLACES.
|
|
180
|
+
#
|
|
181
|
+
# The two blind causes are kept DISTINCT in the detail because they decay
|
|
182
|
+
# differently: a missing stamp is a producer that dropped it (an outage
|
|
183
|
+
# shape); a missing key is a verdict older than the flag (a shape that
|
|
184
|
+
# drains to zero as verdicts are rewritten). A single message would make
|
|
185
|
+
# them indistinguishable, which is the naming rule one level down.
|
|
164
186
|
_vstamp = (card.get("link_stamps") or {}).get("peer_verdict")
|
|
165
|
-
if _vstamp is None:
|
|
187
|
+
if _vstamp is None or "self_authored" not in _vstamp:
|
|
188
|
+
_why = ("no link_stamp for peer_verdict at all — a producer dropped it, "
|
|
189
|
+
"or the verdict predates #199"
|
|
190
|
+
if _vstamp is None else
|
|
191
|
+
f"link_stamp exists (by={_vstamp.get('by')!r}) but carries NO "
|
|
192
|
+
f"self_authored key — the verdict predates the flag")
|
|
166
193
|
add("verdict is not self-authored", None,
|
|
167
|
-
"
|
|
168
|
-
"
|
|
169
|
-
"COULD NOT EVALUATE, never a pass")
|
|
194
|
+
f"{_why}; cannot tell whether the reviewee wrote it. "
|
|
195
|
+
f"COULD NOT EVALUATE, never a pass")
|
|
170
196
|
else:
|
|
171
|
-
add("verdict is not self-authored", not _vstamp
|
|
172
|
-
f"self_authored={_vstamp
|
|
197
|
+
add("verdict is not self-authored", not _vstamp["self_authored"],
|
|
198
|
+
f"self_authored={_vstamp['self_authored']!r} by={_vstamp.get('by')!r} "
|
|
173
199
|
f"— the person being reviewed cannot supply the review")
|
|
174
200
|
add("verdict is APPROVED", verdict["verdict"] == "APPROVED",
|
|
175
201
|
f"verdict={verdict['verdict']}")
|
|
@@ -138,7 +138,21 @@ def _resolve_state_dir(arg: Optional[str], self_name_arg: Optional[str]) -> Path
|
|
|
138
138
|
|
|
139
139
|
|
|
140
140
|
def _resolve_token(token_file_arg: Optional[str]) -> str:
|
|
141
|
-
"""
|
|
141
|
+
"""DELEGATES to onboard's resolver (#243) — and this docstring used to lie.
|
|
142
|
+
|
|
143
|
+
It read "Mirror onboard's resolution. env -> secrets.toml mode 0600 ->
|
|
144
|
+
prompt", which describes a COPY. The body has always been a delegation, so
|
|
145
|
+
the fix for #243 reached this verb for free — but a reader auditing which
|
|
146
|
+
verbs inherited the retired-credential bug would have read this line and
|
|
147
|
+
concluded daemon had its own broken copy. I did exactly that, and carded it.
|
|
148
|
+
|
|
149
|
+
>>> A DOCSTRING THAT DESCRIBES A COPY OVER A BODY THAT DELEGATES IS A STALE
|
|
150
|
+
RECORD, AND IT COSTS THE NEXT READER THE SAME WRONG CONCLUSION. <<< The
|
|
151
|
+
resolution order it names is also now wrong: onboard gained
|
|
152
|
+
~/.config/swarph/<SWARPH_SELF>.peer_token and REFUSES rather than prompting.
|
|
153
|
+
Naming the delegation instead of restating the order means this cannot go
|
|
154
|
+
stale again.
|
|
155
|
+
"""
|
|
142
156
|
from swarph_cli.commands.onboard import _resolve_token as _onboard_resolve
|
|
143
157
|
|
|
144
158
|
return _onboard_resolve(token_file_arg)
|
|
@@ -167,14 +167,51 @@ def _resolve_token(token_file_arg: Optional[str]) -> str:
|
|
|
167
167
|
f"swarph onboard: failed to read {secrets_path}: {exc}", file=sys.stderr
|
|
168
168
|
)
|
|
169
169
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
170
|
+
# ── #243: THE PER-PEER TOKEN — THE CREDENTIAL THAT ACTUALLY EXISTS ──────
|
|
171
|
+
# >>> THIS VERB WAS LOOKING ONLY FOR THE CREDENTIAL THE R1 MIGRATION
|
|
172
|
+
# RETIRED. <<< Measured on lab-ovh 2026-08-03: $MESH_GATEWAY_TOKEN UNSET,
|
|
173
|
+
# ~/.swarph/secrets.toml ABSENT, and 10+ files present at
|
|
174
|
+
# ~/.config/swarph/<peer>.peer_token. Every other verb resolves the per-peer
|
|
175
|
+
# file; onboard (and ratify, which re-exports this function, and daemon,
|
|
176
|
+
# which copied it) were left on the old path — classic mint-vs-cutover, the
|
|
177
|
+
# credential moved and three consumers did not.
|
|
178
|
+
#
|
|
179
|
+
# AND THE GATEWAY NEVER REQUIRED THE SHARED TOKEN: measured, POST
|
|
180
|
+
# /peers/register with a PER-PEER token returns 200 and mints. So this was
|
|
181
|
+
# never a permissions problem — only a lookup that never learned.
|
|
182
|
+
#
|
|
183
|
+
# Placed AFTER the existing two so a working operator-token setup is
|
|
184
|
+
# unchanged; this only fills the hole where the verb used to prompt.
|
|
185
|
+
self_name = os.environ.get("SWARPH_SELF", "").strip()
|
|
186
|
+
if self_name:
|
|
187
|
+
peer_tok = Path.home() / ".config" / "swarph" / f"{self_name}.peer_token"
|
|
188
|
+
if peer_tok.exists():
|
|
189
|
+
try:
|
|
190
|
+
val = peer_tok.read_text(encoding="utf-8").strip()
|
|
191
|
+
if val:
|
|
192
|
+
return val
|
|
193
|
+
except Exception as exc:
|
|
194
|
+
print_safe(f"swarph onboard: failed to read {peer_tok}: {exc}",
|
|
195
|
+
file=sys.stderr)
|
|
196
|
+
|
|
197
|
+
# >>> REFUSE. DO NOT PROMPT. <<< getpass on a non-tty is a guaranteed
|
|
198
|
+
# EOFError, and this verb runs from scripts, cron and spawned cells — the
|
|
199
|
+
# traceback it produced was reported as a BROKEN VERB, not as a missing
|
|
200
|
+
# credential, because the prompt hid which of the two it was.
|
|
201
|
+
#
|
|
202
|
+
# NOTE the deliberate absence of a default for SWARPH_SELF: guessing a name
|
|
203
|
+
# makes a cell hunt ANOTHER CELL'S token, find nothing, and blame the
|
|
204
|
+
# credential — measured on 6 of 6 cells 2026-07-29. Unset is named as unset.
|
|
205
|
+
raise RuntimeError(
|
|
206
|
+
"no gateway credential found. Tried, in order:\n"
|
|
207
|
+
f" 1. --token-file {'(not given)' if not token_file_arg else token_file_arg}\n"
|
|
208
|
+
" 2. $MESH_GATEWAY_TOKEN (unset)\n"
|
|
209
|
+
f" 3. {secrets_path}\n"
|
|
210
|
+
f" 4. ~/.config/swarph/<self>.peer_token "
|
|
211
|
+
+ (f"(SWARPH_SELF={self_name!r} -> not found)" if self_name
|
|
212
|
+
else "(SWARPH_SELF IS UNSET — set it; this verb will not guess a peer name)")
|
|
213
|
+
+ "\n The per-peer token is sufficient: POST /peers/register accepts it."
|
|
176
214
|
)
|
|
177
|
-
return getpass("MESH_GATEWAY_TOKEN: ").strip()
|
|
178
215
|
|
|
179
216
|
|
|
180
217
|
def _post_json(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: swarph-cli
|
|
3
|
-
Version: 0.41.
|
|
3
|
+
Version: 0.41.4
|
|
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
|
|
@@ -94,6 +94,8 @@ src/swarph_cli/service/providers.py
|
|
|
94
94
|
src/swarph_cli/systemd/swarph-watchdog.default
|
|
95
95
|
src/swarph_cli/systemd/swarph-watchdog.service
|
|
96
96
|
src/swarph_cli/systemd/swarph-watchdog.timer
|
|
97
|
+
tests/test_243_onboard_per_peer_token.py
|
|
98
|
+
tests/test_84_cli_labels.py
|
|
97
99
|
tests/test_add_security_gate.py
|
|
98
100
|
tests/test_artifact_add.py
|
|
99
101
|
tests/test_artifact_hash.py
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""#243 — `swarph onboard` resolved ONLY the credential the R1 migration retired.
|
|
2
|
+
|
|
3
|
+
Measured on lab-ovh 2026-08-03: $MESH_GATEWAY_TOKEN UNSET, ~/.swarph/secrets.toml
|
|
4
|
+
ABSENT, and 10+ files present at ~/.config/swarph/<peer>.peer_token. Every other
|
|
5
|
+
verb resolves the per-peer file; onboard (and ratify, which re-exports it, and
|
|
6
|
+
daemon, which COPIED it) were left on the old path.
|
|
7
|
+
|
|
8
|
+
The gateway never required the shared token — POST /peers/register accepts a
|
|
9
|
+
per-peer token and mints (measured). So this was never permissions, only a lookup
|
|
10
|
+
that never learned.
|
|
11
|
+
"""
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from swarph_cli.commands import onboard
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture(autouse=True)
|
|
18
|
+
def _no_ambient_credentials(monkeypatch, tmp_path):
|
|
19
|
+
"""The bug is invisible on a box that still has the old credential, so the
|
|
20
|
+
fixture removes BOTH retired sources — which is the state of every cell
|
|
21
|
+
since the migration."""
|
|
22
|
+
monkeypatch.delenv("MESH_GATEWAY_TOKEN", raising=False)
|
|
23
|
+
monkeypatch.delenv("SWARPH_SELF", raising=False)
|
|
24
|
+
monkeypatch.setattr(onboard.Path, "home", staticmethod(lambda: tmp_path))
|
|
25
|
+
return tmp_path
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _write_peer_token(home, peer, value="tok-per-peer"):
|
|
29
|
+
d = home / ".config" / "swarph"
|
|
30
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
(d / f"{peer}.peer_token").write_text(value, encoding="utf-8")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_it_resolves_the_PER_PEER_token(_no_ambient_credentials, monkeypatch):
|
|
35
|
+
""">>> THE BUG. <<< With no shared token anywhere — the state of every cell
|
|
36
|
+
since R1 — onboard must find the per-peer file rather than prompting."""
|
|
37
|
+
_write_peer_token(_no_ambient_credentials, "lab-ovh")
|
|
38
|
+
monkeypatch.setenv("SWARPH_SELF", "lab-ovh")
|
|
39
|
+
assert onboard._resolve_token(None) == "tok-per-peer"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_it_REFUSES_instead_of_prompting_when_nothing_is_found(_no_ambient_credentials):
|
|
43
|
+
"""getpass on a non-tty is a guaranteed EOFError, and this verb runs from
|
|
44
|
+
scripts, cron and spawned cells. The refusal must NAME the places it looked —
|
|
45
|
+
the prompt is what made a missing credential look like a broken verb."""
|
|
46
|
+
with pytest.raises(RuntimeError) as e:
|
|
47
|
+
onboard._resolve_token(None)
|
|
48
|
+
msg = str(e.value)
|
|
49
|
+
assert "MESH_GATEWAY_TOKEN" in msg
|
|
50
|
+
assert "peer_token" in msg
|
|
51
|
+
assert "SWARPH_SELF IS UNSET" in msg, (
|
|
52
|
+
"an unset SWARPH_SELF must be named as unset — guessing a peer name makes "
|
|
53
|
+
"a cell hunt ANOTHER CELL'S token and blame the credential")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_it_does_NOT_guess_a_peer_name(_no_ambient_credentials):
|
|
57
|
+
"""MEASURED 2026-07-29 on 6 of 6 cells: a default self-name makes a cell look
|
|
58
|
+
for lab-ovh's token, find nothing, and report a TOKEN fault. Harmless on
|
|
59
|
+
lab-ovh, silently wrong everywhere else."""
|
|
60
|
+
_write_peer_token(_no_ambient_credentials, "lab-ovh") # present but not ours
|
|
61
|
+
with pytest.raises(RuntimeError):
|
|
62
|
+
onboard._resolve_token(None) # SWARPH_SELF unset
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_the_SHARED_token_still_wins_when_present(_no_ambient_credentials, monkeypatch):
|
|
66
|
+
"""NON-VACUITY. The per-peer path is ADDITIVE — an operator with a working
|
|
67
|
+
shared-token setup must be unaffected, or the fix trades one broken
|
|
68
|
+
population for another."""
|
|
69
|
+
_write_peer_token(_no_ambient_credentials, "lab-ovh", "tok-per-peer")
|
|
70
|
+
monkeypatch.setenv("SWARPH_SELF", "lab-ovh")
|
|
71
|
+
monkeypatch.setenv("MESH_GATEWAY_TOKEN", "tok-shared")
|
|
72
|
+
assert onboard._resolve_token(None) == "tok-shared"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_ratify_and_daemon_resolve_through_THE_SAME_function():
|
|
76
|
+
""">>> ratify RE-EXPORTED and was fixed for free; daemon COPIED and was not.
|
|
77
|
+
<<< A copy made to keep two things identical keeps them identical only until
|
|
78
|
+
one is fixed. Both now delegate, so the next credential migration cannot
|
|
79
|
+
break one and not the others."""
|
|
80
|
+
from swarph_cli.commands import ratify, daemon
|
|
81
|
+
import inspect
|
|
82
|
+
for mod in (ratify, daemon):
|
|
83
|
+
src = inspect.getsource(mod._resolve_token)
|
|
84
|
+
assert "from swarph_cli.commands.onboard import _resolve_token" in src, (
|
|
85
|
+
f"{mod.__name__} does not delegate — it has its own copy and will "
|
|
86
|
+
"diverge at the next migration")
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""#84 CLI half — `--label` filter, `--label` on add, `cards label add/rm`.
|
|
2
|
+
|
|
3
|
+
The gateway takes a FULL REPLACEMENT for labels (they are a set — a merge-only
|
|
4
|
+
field could never REMOVE one), so `label rm` is unavoidably read-modify-write.
|
|
5
|
+
The risk that creates is the one these tests pin: a replacement built on a FAILED
|
|
6
|
+
read would silently drop every other label on the card.
|
|
7
|
+
"""
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from swarph_cli.commands import board as b
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# ── the pure transform, so the RMW is testable without a gateway ────────────
|
|
14
|
+
|
|
15
|
+
@pytest.mark.parametrize("cur,action,lab,want", [
|
|
16
|
+
([], "add", "obligation-ledger", ["obligation-ledger"]),
|
|
17
|
+
(["a"], "add", "b", ["a", "b"]),
|
|
18
|
+
(["a", "b"], "rm", "a", ["b"]),
|
|
19
|
+
(["a"], "rm", "nope", ["a"]), # removing an absent label is a no-op
|
|
20
|
+
(["a"], "add", "a", ["a"]), # idempotent add
|
|
21
|
+
(["a"], "add", "A", ["a"]), # case is a WRITING variation, not a new label
|
|
22
|
+
(["A", "B"], "rm", "a", ["b"]), # ...on both sides
|
|
23
|
+
(None, "add", "x", ["x"]), # a card with no labels column yet
|
|
24
|
+
])
|
|
25
|
+
def test_apply_label(cur, action, lab, want):
|
|
26
|
+
assert b._apply_label(cur, action, lab) == want
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_add_is_idempotent_and_order_preserving():
|
|
30
|
+
"""Labels are a SET but the ORDER is what a human reads. Re-adding must not
|
|
31
|
+
reorder — a card whose labels shuffle on every touch is noise in the diff."""
|
|
32
|
+
out = ["one", "two", "three"]
|
|
33
|
+
for lab in ("one", "two", "three"):
|
|
34
|
+
out = b._apply_label(out, "add", lab)
|
|
35
|
+
assert out == ["one", "two", "three"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ── the URL + payload builders ─────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
def test_list_url_carries_the_label_filter():
|
|
41
|
+
url = b._cards_list_url("http://gw", project=8, label="obligation-ledger")
|
|
42
|
+
assert "label=obligation-ledger" in url
|
|
43
|
+
assert "project=8" in url
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_list_url_omits_the_filter_when_absent():
|
|
47
|
+
"""NON-VACUITY: an empty filter must not become `?label=`, which the gateway
|
|
48
|
+
would read as a request for cards labelled with the empty string."""
|
|
49
|
+
assert "label" not in b._cards_list_url("http://gw", project=8)
|
|
50
|
+
assert "label" not in b._cards_list_url("http://gw", project=8, label=None)
|
|
51
|
+
assert "label" not in b._cards_list_url("http://gw", project=8, label="")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_add_payload_carries_labels_only_when_given():
|
|
55
|
+
assert "labels" not in b._card_add_payload("me", 8, "t")
|
|
56
|
+
p = b._card_add_payload("me", 8, "t", labels=["a", "b"])
|
|
57
|
+
assert p["labels"] == ["a", "b"]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ── >>> THE CLOBBER GUARD — the reason the RMW is safe <<< ──────────────────
|
|
61
|
+
|
|
62
|
+
def test_label_REFUSES_to_write_when_the_read_FAILS(monkeypatch, capsys):
|
|
63
|
+
""">>> A REPLACEMENT BUILT ON A FAILED READ WOULD DROP EVERY OTHER LABEL. <<<
|
|
64
|
+
|
|
65
|
+
`label add` must GET the card, compute the new set, then PATCH the whole
|
|
66
|
+
array. If the GET fails and the code proceeds with `[]` as "current", the
|
|
67
|
+
PATCH silently erases the card's other labels — a data-loss bug that looks
|
|
68
|
+
like a successful write.
|
|
69
|
+
|
|
70
|
+
Pinned by making the GET fail and asserting NOTHING is patched.
|
|
71
|
+
"""
|
|
72
|
+
patched = []
|
|
73
|
+
monkeypatch.setattr(b, "_http_get_json", lambda *a, **k: (503, {"detail": "down"}))
|
|
74
|
+
monkeypatch.setattr(b, "_patch_json", lambda *a, **k: patched.append(a) or (200, {}))
|
|
75
|
+
monkeypatch.setattr(b, "_resolve_self_name", lambda *_: "lab-ovh")
|
|
76
|
+
monkeypatch.setattr(b, "_resolve_token", lambda *_: "tok")
|
|
77
|
+
|
|
78
|
+
rc = b.run_board(["cards", "label", "add", "1", "x", "--gateway", "http://gw"])
|
|
79
|
+
assert rc == 1, "a failed read must refuse, not fall through to a write"
|
|
80
|
+
assert patched == [], "PATCHED after a failed GET — the other labels were clobbered"
|
|
81
|
+
assert "refusing to write" in capsys.readouterr().err
|
|
@@ -347,3 +347,50 @@ def test_a_READY_card_with_NO_peer_verdict_REFUSES():
|
|
|
347
347
|
d = mc.decide(_card(links={"pr": "https://github.com/o/r/pull/152"}), _pr())
|
|
348
348
|
assert _fail(d, "peer verdict recorded on the card")
|
|
349
349
|
assert d.verdict != "WOULD_MERGE"
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def test_a_stamp_WITHOUT_the_self_authored_KEY_abstains_like_no_stamp():
|
|
353
|
+
""">>> THE BUG THAT SHIPPED IN 0.41.2, AND IT WAS 92% OF PRODUCTION. <<<
|
|
354
|
+
|
|
355
|
+
Found by droplet RUNNING decide() on the installed package instead of reading
|
|
356
|
+
it. The first version tested `_vstamp is None` — absence of the STAMP. The
|
|
357
|
+
backlog's real shape is a stamp that EXISTS (written before self_authored
|
|
358
|
+
shipped) and lacks the KEY: it fell through to the comparison, where
|
|
359
|
+
`.get("self_authored")` returns None, `not None` is True, and A TEXTBOOK
|
|
360
|
+
SELF-REVIEW PASSED.
|
|
361
|
+
|
|
362
|
+
Measured at the time: 53 link_stamps in production, 4 carrying the flag — 49
|
|
363
|
+
(92%) were this shape. The leg was VACUOUS FOR EXACTLY THE BACKLOG IT WAS
|
|
364
|
+
BUILT FOR, which is the outcome the design explicitly targets and which I had
|
|
365
|
+
reported to the date-holder as avoided. He authorised near-total ABSTENTION;
|
|
366
|
+
what shipped was near-total PASS.
|
|
367
|
+
|
|
368
|
+
WHY THE VERIFICATION MISSED IT: the wheel audit and the end-to-end check
|
|
369
|
+
covered self_authored=True (refuses) and no-stamp (abstains). Both correct,
|
|
370
|
+
NEITHER IS THE PRODUCTION SHAPE. The untested case was the only one that
|
|
371
|
+
exists in the data.
|
|
372
|
+
"""
|
|
373
|
+
stamp_no_key = {"peer_verdict": {"by": "droplet", "caller_bound": True,
|
|
374
|
+
"at": "2026-08-01T00:00:00Z"}}
|
|
375
|
+
d = mc.decide(_card(created_by="lab-ovh", assignee="droplet",
|
|
376
|
+
link_stamps=stamp_no_key), _pr())
|
|
377
|
+
check = [c for c in d.checks if c.name == "verdict is not self-authored"][0]
|
|
378
|
+
assert check.ok is None, (
|
|
379
|
+
"a stamp lacking the self_authored KEY passed the self-review leg — "
|
|
380
|
+
"vacuous for 92% of production")
|
|
381
|
+
assert d.verdict != "WOULD_MERGE"
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def test_the_two_BLIND_causes_are_named_distinctly():
|
|
385
|
+
"""They decay differently and must not print the same: a MISSING STAMP is a
|
|
386
|
+
producer that dropped it (outage shape); a MISSING KEY is a verdict older than
|
|
387
|
+
the flag (drains to zero as verdicts are rewritten). One message for both
|
|
388
|
+
would make a permanent outage indistinguishable from a shrinking backlog."""
|
|
389
|
+
no_stamp = mc.decide(_card(created_by="lab-ovh", link_stamps={}), _pr())
|
|
390
|
+
no_key = mc.decide(_card(created_by="lab-ovh",
|
|
391
|
+
link_stamps={"peer_verdict": {"by": "droplet"}}), _pr())
|
|
392
|
+
d1 = [c for c in no_stamp.checks if c.name == "verdict is not self-authored"][0].detail
|
|
393
|
+
d2 = [c for c in no_key.checks if c.name == "verdict is not self-authored"][0].detail
|
|
394
|
+
assert d1 != d2, "both blind causes printed the same detail"
|
|
395
|
+
assert "at all" in d1
|
|
396
|
+
assert "predates the flag" in d2
|
|
@@ -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.41.
|
|
20
|
+
assert swarph_cli.__version__ == "0.41.4"
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
# --- request / response plumbing -------------------------------------------
|
|
@@ -48,14 +48,30 @@ def test_resolve_token_warns_on_loose_mode(monkeypatch, tmp_path, capsys):
|
|
|
48
48
|
assert "0o644" in err
|
|
49
49
|
|
|
50
50
|
|
|
51
|
-
def
|
|
51
|
+
def test_resolve_token_REFUSES_rather_than_prompting(monkeypatch, tmp_path):
|
|
52
|
+
""">>> THIS TEST USED TO ASSERT THE PROMPT, AND THE PROMPT WAS THE BUG (#243).
|
|
53
|
+
<<<
|
|
54
|
+
|
|
55
|
+
It pinned `falls_back_to_prompt` — correct when a shared MESH_GATEWAY_TOKEN
|
|
56
|
+
was the only credential. After the R1 per-peer migration neither
|
|
57
|
+
$MESH_GATEWAY_TOKEN nor ~/.swarph/secrets.toml exists on any cell, so the
|
|
58
|
+
prompt became the DEFAULT path — and getpass on a non-tty is a guaranteed
|
|
59
|
+
EOFError. `swarph onboard` was reported as a broken verb; it was a missing
|
|
60
|
+
credential wearing a traceback.
|
|
61
|
+
|
|
62
|
+
The expectation is flipped DELIBERATELY: refuse, naming every place it
|
|
63
|
+
looked, so the operator learns which credential is absent instead of meeting
|
|
64
|
+
a prompt that cannot be answered.
|
|
65
|
+
"""
|
|
52
66
|
monkeypatch.delenv("MESH_GATEWAY_TOKEN", raising=False)
|
|
53
|
-
monkeypatch.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
assert "
|
|
67
|
+
monkeypatch.delenv("SWARPH_SELF", raising=False)
|
|
68
|
+
monkeypatch.setattr(onboard.Path, "home", staticmethod(lambda: tmp_path))
|
|
69
|
+
with pytest.raises(RuntimeError) as e:
|
|
70
|
+
onboard._resolve_token(str(tmp_path / "no-such-file.toml"))
|
|
71
|
+
msg = str(e.value)
|
|
72
|
+
assert "MESH_GATEWAY_TOKEN" in msg and "peer_token" in msg, (
|
|
73
|
+
"the refusal must name every place it looked — a bare failure leaves the "
|
|
74
|
+
"operator guessing which of four credentials is missing")
|
|
59
75
|
|
|
60
76
|
|
|
61
77
|
# ---------------------------------------------------------------------------
|
|
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
|