swarph-cli 0.41.3__tar.gz → 0.41.7__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.3/src/swarph_cli.egg-info → swarph_cli-0.41.7}/PKG-INFO +30 -2
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/README.md +28 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/pyproject.toml +18 -2
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/__init__.py +1 -1
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/board.py +176 -6
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/daemon.py +15 -1
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/mesh.py +60 -64
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/monitor.py +2 -1
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/onboard.py +130 -14
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/ratify.py +7 -2
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/spawn.py +405 -49
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/gateway/lanes_control.py +7 -1
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/main.py +15 -4
- swarph_cli-0.41.7/src/swarph_cli/tokens.py +127 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7/src/swarph_cli.egg-info}/PKG-INFO +30 -2
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli.egg-info/SOURCES.txt +6 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli.egg-info/requires.txt +1 -1
- swarph_cli-0.41.7/tests/test_243_onboard_per_peer_token.py +86 -0
- swarph_cli-0.41.7/tests/test_84_cli_labels.py +81 -0
- swarph_cli-0.41.7/tests/test_board_card_thread.py +171 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_brain_ask_command.py +2 -2
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_monitor_state_split.py +21 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_onboard_command.py +23 -7
- swarph_cli-0.41.7/tests/test_spawn_base_hoist.py +206 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_spawn_command.py +14 -5
- swarph_cli-0.41.7/tests/test_token_precedence_explicit_first.py +219 -0
- swarph_cli-0.41.7/tests/test_vibe_membrane.py +324 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_watchdog.py +2 -2
- swarph_cli-0.41.3/tests/test_spawn_base_hoist.py +0 -79
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/LICENSE +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/setup.cfg +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/bench/__init__.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/bench/backends.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/bench/pack.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/bench/prices.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/bench/quality.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/bench/refresh_prices.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/bench/runner.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/bench/validate.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/caller.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/capture/__init__.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/capture/harden.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/capture/lineage.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/capture/liveness.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/capture/manifest.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/capture/paths.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/capture/verify.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/cell.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/chain_token.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/__init__.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/_display.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/_gateway_client.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/add.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/bench.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/board_merge_check.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/brain.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/brain_ask.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/cell.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/cell_selfcheck.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/channel.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/chat.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/codegraph.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/codegraph_hook.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/compress.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/event.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/gateway.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/group.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/highlight.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/hook_output.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/hooks.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/import_session.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/init.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/install_hook.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/install_multiplexer.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/lane.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/mcp_server.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/memory.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/memory_sync.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/okf_links.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/protocol_handler.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/schedule.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/security.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/service.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/timeline.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/commands/watchdog.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/compress/__init__.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/compress/levers.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/compress/marker.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/compress/verify.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/console_safe.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/delivery_queue.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/gateway/__init__.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/gateway/feature_registry.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/gateway/schema.sql +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/gateway/server.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/gateway/services_control.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/multiplexer.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/parsers/__init__.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/parsers/claude.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/scripts/ensure_monitor.sh +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/service/__init__.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/service/app.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/service/providers.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/session_bridge.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/stall_alert.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli.egg-info/entry_points.txt +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/src/swarph_cli.egg-info/top_level.txt +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_add_security_gate.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_artifact_add.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_artifact_hash.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_artifact_lib.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_artifact_mcp.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_artifact_skill.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_artifact_tool.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_artifact_uri.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_bench_backends.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_bench_command.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_bench_pack.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_bench_prices.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_bench_quality.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_bench_runner.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_bench_validate.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_board_command.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_board_merge_check.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_brain_ask_env_diagnosis.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_brain_command.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_caller_meta_guard.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_capture_lineage.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_capture_liveness.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_capture_manifest.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_capture_paths.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_capture_security.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_cell_command_dispatch.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_cell_harden.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_cell_loader.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_cell_selfcheck.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_cell_verify.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_chain_token.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_channel_command.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_channel_post.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_chat_command.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_claude_parser.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_claude_tmux_template.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_codegraph_caller_scope.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_codegraph_command.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_codegraph_hook.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_codegraph_mcp.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_compress_caller_convention.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_compress_command.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_compress_levers.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_compress_marker.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_compress_verify.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_console_encoding_e2e.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_daemon_command.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_daemon_console_encoding.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_daemon_delivery.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_delivery_queue.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_display_sanitize.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_event_emit.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_feature_to_uri.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_gateway_brain.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_gateway_client.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_gateway_command.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_group_command.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_highlight_command.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_hook_output.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_hooks_activity_marker.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_hooks_add.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_hooks_bundle.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_hooks_init.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_hooks_lifecycle.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_hooks_merge.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_import_command.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_init_command.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_install_hook.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_install_multiplexer_command.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_lane_command.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_main.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_main_unknown_verb_guard.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_mcp_server.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_memory_command.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_memory_gateway.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_memory_sync.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_memory_sync_membrane.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_mesh_command.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_mesh_inbox_mark_read.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_mesh_inbox_sanitize.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_mesh_sidecar.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_mesh_sidecar_cursor_decoupled.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_mesh_sidecar_wake_survives_read.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_meta_edge_identity.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_monitor_cli.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_monitor_daemon_collision.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_monitor_self_name_verification.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_multiplexer.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_okf_links.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_packaged_artifacts_exist.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_process_liveness.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_protocol_handler.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_providers_isolation.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_ratify_command.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_repl_caller_convention.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_schedule_command.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_schedule_create_contract.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_security.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_service_app.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_service_command.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_service_providers.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_session_bridge_inject.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_session_bridge_probe.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_session_bridge_resolve.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_session_path_role_gate.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_shipped_units_match_the_cli.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_smoke_chat.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_smoke_one_shot.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_smoke_phase_5_5.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_spawn_live_pin.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_spawn_mitosis_lineage.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_spawn_tmux_session.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_spawn_windows_relaunch.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_stall_alert.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_timeline_command.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_token_file_one_parser.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_version_consistency.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_watchdog_dm_wake.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_watchdog_dm_wake_wiring.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_watchdog_emit_health.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_watchdog_install_flags.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/tests/test_watchdog_model_rung.py +0 -0
- {swarph_cli-0.41.3 → swarph_cli-0.41.7}/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.7
|
|
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
|
|
@@ -24,7 +24,7 @@ Requires-Python: >=3.10
|
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
License-File: LICENSE
|
|
26
26
|
Requires-Dist: swarph-mesh>=0.5.0
|
|
27
|
-
Requires-Dist: swarph-shared
|
|
27
|
+
Requires-Dist: swarph-shared!=0.6.0,<0.7,>=0.4.0
|
|
28
28
|
Requires-Dist: PyYAML>=6.0
|
|
29
29
|
Provides-Extra: dev
|
|
30
30
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
@@ -108,10 +108,38 @@ swarph board cards add --project <id|slug> --title "…" [--body "…"] [--ai2]
|
|
|
108
108
|
swarph board cards move <id> <stage> # advance the card (proposed→idea→spec→plan→build→test→done)
|
|
109
109
|
swarph board cards link <id> <key> <value> # add/update a link (merges — never clobbers existing links)
|
|
110
110
|
swarph board cards assign <id> <who>
|
|
111
|
+
swarph board cards thread <id> [--limit N] # the card's conversation (a card IS a thread)
|
|
112
|
+
swarph board cards say <id> --content "…" [--to <peer>] [--kind fyi|question|answer|status|unblock]
|
|
111
113
|
```
|
|
112
114
|
|
|
113
115
|
`--project` accepts a numeric id **or** a slug (resolved via `projects list`). Note: card creation always starts at `proposed` (the gateway has no stage-on-create) — use `cards move` to advance.
|
|
114
116
|
|
|
117
|
+
**`thread` / `say` (v0.41.6)** — every card carries a bound conversation, so a
|
|
118
|
+
finding, a verdict, or a hand-off lands **on the card** instead of in a DM only the
|
|
119
|
+
sender and one recipient can read.
|
|
120
|
+
|
|
121
|
+
`say` needs a recipient because a card post is still a DM on the wire: `--to` wins,
|
|
122
|
+
otherwise the card's **assignee**. With neither, it **refuses** rather than
|
|
123
|
+
substituting a placeholder — the gateway accepts an unregistered `to_node` with a
|
|
124
|
+
`200`, so a placeholder would be indistinguishable from delivery while being
|
|
125
|
+
addressed to nobody.
|
|
126
|
+
|
|
127
|
+
Attaching requires an explicit `propose` grant on the project, not merely read
|
|
128
|
+
access: a message on a card publishes to everyone who can read that card, now and
|
|
129
|
+
in future.
|
|
130
|
+
|
|
131
|
+
> **A card thread is a mesh-wide surface, not a two-party one.** 13 of 14 projects
|
|
132
|
+
> carry `group:ALL`, so a card's conversation is readable by **every ratified peer**.
|
|
133
|
+
> That is the design — a card's discussion belongs on the card regardless of who
|
|
134
|
+
> sent it — and it is the same fact as the feature's selling point, stated from the
|
|
135
|
+
> other side.
|
|
136
|
+
>
|
|
137
|
+
> **Put the verdict on the card. Not the credential.** Findings, decisions,
|
|
138
|
+
> hand-offs and review outcomes belong there and are better there than in one
|
|
139
|
+
> inbox. Tokens, keys and client-confidential material are a DM or nothing.
|
|
140
|
+
> There is no retraction: the message is a row, and *now and in future* means a
|
|
141
|
+
> peer ratified next month reads what you post today.
|
|
142
|
+
|
|
115
143
|
### `swarph bench` (v0.38.0)
|
|
116
144
|
|
|
117
145
|
A **deterministic** LLM benchmark: run an N-way model *showdown* on a **pack** — a self-contained, subject-agnostic unit of `{system, tasks, expected}` — and report quality (ground-truth **distance**, not vibes) alongside tokens / cost / latency. You define the correct answer; the score is math; anyone re-runs = the same number.
|
|
@@ -62,10 +62,38 @@ swarph board cards add --project <id|slug> --title "…" [--body "…"] [--ai2]
|
|
|
62
62
|
swarph board cards move <id> <stage> # advance the card (proposed→idea→spec→plan→build→test→done)
|
|
63
63
|
swarph board cards link <id> <key> <value> # add/update a link (merges — never clobbers existing links)
|
|
64
64
|
swarph board cards assign <id> <who>
|
|
65
|
+
swarph board cards thread <id> [--limit N] # the card's conversation (a card IS a thread)
|
|
66
|
+
swarph board cards say <id> --content "…" [--to <peer>] [--kind fyi|question|answer|status|unblock]
|
|
65
67
|
```
|
|
66
68
|
|
|
67
69
|
`--project` accepts a numeric id **or** a slug (resolved via `projects list`). Note: card creation always starts at `proposed` (the gateway has no stage-on-create) — use `cards move` to advance.
|
|
68
70
|
|
|
71
|
+
**`thread` / `say` (v0.41.6)** — every card carries a bound conversation, so a
|
|
72
|
+
finding, a verdict, or a hand-off lands **on the card** instead of in a DM only the
|
|
73
|
+
sender and one recipient can read.
|
|
74
|
+
|
|
75
|
+
`say` needs a recipient because a card post is still a DM on the wire: `--to` wins,
|
|
76
|
+
otherwise the card's **assignee**. With neither, it **refuses** rather than
|
|
77
|
+
substituting a placeholder — the gateway accepts an unregistered `to_node` with a
|
|
78
|
+
`200`, so a placeholder would be indistinguishable from delivery while being
|
|
79
|
+
addressed to nobody.
|
|
80
|
+
|
|
81
|
+
Attaching requires an explicit `propose` grant on the project, not merely read
|
|
82
|
+
access: a message on a card publishes to everyone who can read that card, now and
|
|
83
|
+
in future.
|
|
84
|
+
|
|
85
|
+
> **A card thread is a mesh-wide surface, not a two-party one.** 13 of 14 projects
|
|
86
|
+
> carry `group:ALL`, so a card's conversation is readable by **every ratified peer**.
|
|
87
|
+
> That is the design — a card's discussion belongs on the card regardless of who
|
|
88
|
+
> sent it — and it is the same fact as the feature's selling point, stated from the
|
|
89
|
+
> other side.
|
|
90
|
+
>
|
|
91
|
+
> **Put the verdict on the card. Not the credential.** Findings, decisions,
|
|
92
|
+
> hand-offs and review outcomes belong there and are better there than in one
|
|
93
|
+
> inbox. Tokens, keys and client-confidential material are a DM or nothing.
|
|
94
|
+
> There is no retraction: the message is a row, and *now and in future* means a
|
|
95
|
+
> peer ratified next month reads what you post today.
|
|
96
|
+
|
|
69
97
|
### `swarph bench` (v0.38.0)
|
|
70
98
|
|
|
71
99
|
A **deterministic** LLM benchmark: run an N-way model *showdown* on a **pack** — a self-contained, subject-agnostic unit of `{system, tasks, expected}` — and report quality (ground-truth **distance**, not vibes) alongside tokens / cost / latency. You define the correct answer; the score is math; anyone re-runs = the same number.
|
|
@@ -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.7"
|
|
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" }
|
|
@@ -40,7 +40,23 @@ dependencies = [
|
|
|
40
40
|
# >=0.3.1 is load-bearing: 0.3.1 is the first release whose
|
|
41
41
|
# scrub_env_for_subprocess closes the ANTHROPIC_AUTH_TOKEN/*_BASE_URL
|
|
42
42
|
# billing-redirect class that the membrane env builders delegate to.
|
|
43
|
-
|
|
43
|
+
# >>> UPPER BOUND IS LOAD-BEARING, NOT TIDINESS. Without it ANY swarph-shared
|
|
44
|
+
# release can break ANY already-released swarph-cli on FRESH installs, with no
|
|
45
|
+
# action by the user and no signal to either package — measured 2026-08-05:
|
|
46
|
+
# shared 0.6.0 added a provider with no membrane here, spawn.py's
|
|
47
|
+
# `VALID_PROVIDERS ⊆ MEMBRANES` guard raised AT IMPORT, and every fresh
|
|
48
|
+
# `pip install swarph-cli` got a dead `swarph spawn` for ~5h.
|
|
49
|
+
# >>> AND THE HARDENED RITUAL MADE IT WORSE: `pipx install --force
|
|
50
|
+
# swarph-cli==X.Y.Z` RE-RESOLVES the transitive dep, so pinning exactly what
|
|
51
|
+
# we told cells to pin did NOT protect them. The most diligent cells were the
|
|
52
|
+
# most exposed. <<<
|
|
53
|
+
# A bound converts a silent runtime break into a RESOLVER CONFLICT AT INSTALL
|
|
54
|
+
# TIME: loud, local, and before anything is running. Raise it deliberately
|
|
55
|
+
# when a shared release is verified against this package. Board #308.
|
|
56
|
+
# `!=0.6.0` is not belt-and-braces: 0.6.0 is KNOWN-BROKEN with this package
|
|
57
|
+
# (it whitelists `vibe`, which has no membrane here, and spawn.py raises at
|
|
58
|
+
# import). Without the exclusion a resolver may still legally select it.
|
|
59
|
+
"swarph-shared>=0.4.0,!=0.6.0,<0.7",
|
|
44
60
|
# Phase 7 spawn — cell.yaml parser. PyYAML 6.x is the standard.
|
|
45
61
|
# Stays a swarph-cli dep since file I/O is operator-tooling-layer
|
|
46
62
|
# concern; swarph-shared cell module is pure-stdlib.
|
|
@@ -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:
|
|
@@ -138,6 +161,71 @@ def _format_projects(data) -> str:
|
|
|
138
161
|
return "\n".join(lines)
|
|
139
162
|
|
|
140
163
|
|
|
164
|
+
# ── #181: the card IS a thread ────────────────────────────────────────────────
|
|
165
|
+
# The gateway has carried `GET /board/cards/{id}/thread` and the card-gated attach
|
|
166
|
+
# path since 2026-08-05, and NOTHING COULD REACH THEM. No CLI verb existed, so the
|
|
167
|
+
# card↔DM fusion lived entirely in the database and the OpenAPI schema: shipped,
|
|
168
|
+
# deployed, and invisible to every human and cell. Found the way these are always
|
|
169
|
+
# found — I tried to write a finding onto a card and had to send a DM instead.
|
|
170
|
+
|
|
171
|
+
def _thread_url(gateway: str, card_id: int, *, limit: Optional[int] = None) -> str:
|
|
172
|
+
base = f"{gateway.rstrip('/')}/board/cards/{card_id}/thread"
|
|
173
|
+
return f"{base}?limit={limit}" if limit else base
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _thread_recipient(card: dict, explicit_to: Optional[str]) -> str:
|
|
177
|
+
"""Who a card-thread post is addressed to. `--to` wins, else the assignee.
|
|
178
|
+
|
|
179
|
+
>>> RAISES RATHER THAN INVENTING A SENTINEL. <<< POST /messages requires
|
|
180
|
+
exactly one of {to_node, channel}, so a card post needs a real recipient. The
|
|
181
|
+
tempting move is a placeholder like "board" or "__card__" — but board card
|
|
182
|
+
#259 measured what the gateway does with an unregistered to_node: it returns
|
|
183
|
+
200 and the message is addressed to nobody, indistinguishable from delivery.
|
|
184
|
+
A placeholder here would manufacture that defect once per card post.
|
|
185
|
+
"""
|
|
186
|
+
if explicit_to:
|
|
187
|
+
return explicit_to
|
|
188
|
+
assignee = card.get("assignee")
|
|
189
|
+
if assignee:
|
|
190
|
+
return assignee
|
|
191
|
+
raise RuntimeError(
|
|
192
|
+
f"card #{card.get('id')} has no assignee, so there is no default recipient "
|
|
193
|
+
f"for a thread post — pass --to <peer>. (A card post is still a DM on the "
|
|
194
|
+
f"wire; it needs someone to be addressed to.)"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _card_say_payload(from_node: str, to_node: str, kind: str, content: str,
|
|
199
|
+
thread_uuid: str) -> dict:
|
|
200
|
+
return {
|
|
201
|
+
"from_node": from_node,
|
|
202
|
+
"to_node": to_node,
|
|
203
|
+
"kind": kind,
|
|
204
|
+
"content": content,
|
|
205
|
+
"thread_id": thread_uuid,
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _format_thread(data) -> str:
|
|
210
|
+
msgs = data.get("messages", []) if isinstance(data, dict) else (data or [])
|
|
211
|
+
card_id = data.get("card_id") if isinstance(data, dict) else None
|
|
212
|
+
if not msgs:
|
|
213
|
+
# NOT "(no messages)" alone — an empty thread and an unreadable one must not
|
|
214
|
+
# render the same. The gateway 403s an unreadable card and 409s an unmigrated
|
|
215
|
+
# one, so reaching here with [] genuinely means nobody has posted yet.
|
|
216
|
+
return f"card #{card_id}: thread is empty — no messages posted yet"
|
|
217
|
+
lines = [f"card #{card_id} — {len(msgs)} message(s)"]
|
|
218
|
+
for m in msgs:
|
|
219
|
+
ts = (m.get("created_at") or "")[:16].replace("T", " ")
|
|
220
|
+
lines.append(
|
|
221
|
+
f"\n [{m.get('id')}] {ts} {_s(m.get('from_node'))} -> "
|
|
222
|
+
f"{_s(m.get('to_node'))} ({_s(m.get('kind'))})"
|
|
223
|
+
)
|
|
224
|
+
for ln in (_s(m.get("content")) or "").splitlines():
|
|
225
|
+
lines.append(f" {ln}")
|
|
226
|
+
return "\n".join(lines)
|
|
227
|
+
|
|
228
|
+
|
|
141
229
|
# ── parser + dispatch ─────────────────────────────────────────────────────────
|
|
142
230
|
|
|
143
231
|
def _build_parser() -> argparse.ArgumentParser:
|
|
@@ -153,13 +241,17 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
153
241
|
cards = top.add_parser("cards", help="board cards").add_subparsers(dest="command", required=True)
|
|
154
242
|
cl = cards.add_parser("list", help="list cards")
|
|
155
243
|
cl.add_argument("--project"); cl.add_argument("--stage"); cl.add_argument("--assignee")
|
|
244
|
+
cl.add_argument("--label", help="only cards carrying this label (exact match)")
|
|
156
245
|
cl.add_argument("--json", action="store_true"); _add_common(cl)
|
|
157
246
|
cs = cards.add_parser("show", help="show one card"); cs.add_argument("id", type=int)
|
|
158
247
|
cs.add_argument("--json", action="store_true"); _add_common(cs)
|
|
159
248
|
ca = cards.add_parser("add", help="create a card")
|
|
160
249
|
ca.add_argument("--project", required=True, help="project id or slug"); ca.add_argument("--title", required=True)
|
|
161
250
|
ca.add_argument("--body"); ca.add_argument("--ai2", action="store_true")
|
|
162
|
-
ca.add_argument("--priority", type=int, default=0)
|
|
251
|
+
ca.add_argument("--priority", type=int, default=0)
|
|
252
|
+
ca.add_argument("--label", action="append", dest="labels", metavar="LABEL",
|
|
253
|
+
help="attach a label (repeatable)")
|
|
254
|
+
ca.add_argument("--json", action="store_true"); _add_common(ca)
|
|
163
255
|
cm = cards.add_parser("move", help="move a card to a stage")
|
|
164
256
|
cm.add_argument("id", type=int); cm.add_argument("stage"); cm.add_argument("--json", action="store_true"); _add_common(cm)
|
|
165
257
|
ck = cards.add_parser("link", help="add/update a link on a card (merges)")
|
|
@@ -167,6 +259,22 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
167
259
|
ck.add_argument("--json", action="store_true"); _add_common(ck)
|
|
168
260
|
cn = cards.add_parser("assign", help="set a card's assignee")
|
|
169
261
|
cn.add_argument("id", type=int); cn.add_argument("assignee"); cn.add_argument("--json", action="store_true"); _add_common(cn)
|
|
262
|
+
cb = cards.add_parser("label", help="add/remove a label on a card")
|
|
263
|
+
cb.add_argument("action", choices=["add", "rm"])
|
|
264
|
+
cb.add_argument("id", type=int); cb.add_argument("label")
|
|
265
|
+
cb.add_argument("--json", action="store_true"); _add_common(cb)
|
|
266
|
+
ct = cards.add_parser("thread", help="show the card's conversation (#181: a card IS a thread)")
|
|
267
|
+
ct.add_argument("id", type=int)
|
|
268
|
+
ct.add_argument("--limit", type=int, default=None, help="max messages (gateway caps at 1000)")
|
|
269
|
+
_add_common(ct)
|
|
270
|
+
cy = cards.add_parser("say", help="post a message onto the card's thread")
|
|
271
|
+
cy.add_argument("id", type=int)
|
|
272
|
+
cy.add_argument("--content", required=True)
|
|
273
|
+
cy.add_argument("--kind", default="fyi", help="status|question|answer|unblock|fyi")
|
|
274
|
+
cy.add_argument("--to", dest="to_node", default=None,
|
|
275
|
+
help="recipient peer (default: the card's assignee)")
|
|
276
|
+
_add_common(cy)
|
|
277
|
+
|
|
170
278
|
cr = cards.add_parser("ready", help="flag a card ready-to-advance (move_ready) for the orchestrator")
|
|
171
279
|
cr.add_argument("id", type=int); cr.add_argument("--clear", action="store_true", help="unset move_ready")
|
|
172
280
|
cr.add_argument("--json", action="store_true"); _add_common(cr)
|
|
@@ -224,7 +332,9 @@ def run_board(argv: list[str]) -> int:
|
|
|
224
332
|
if err:
|
|
225
333
|
print(f"swarph board: {err}", file=sys.stderr)
|
|
226
334
|
return 1
|
|
227
|
-
st, d = _http_get_json(_cards_list_url(gw, project=pid, stage=args.stage,
|
|
335
|
+
st, d = _http_get_json(_cards_list_url(gw, project=pid, stage=args.stage,
|
|
336
|
+
assignee=args.assignee,
|
|
337
|
+
label=getattr(args, "label", None)), token)
|
|
228
338
|
return _out(st, d, _format_cards, aj)
|
|
229
339
|
if args.command == "show":
|
|
230
340
|
st, d = _http_get_json(f"{gw}/board/cards/{args.id}", token)
|
|
@@ -234,14 +344,74 @@ def run_board(argv: list[str]) -> int:
|
|
|
234
344
|
if err or pid is None:
|
|
235
345
|
print(f"swarph board: {err or 'project required'}", file=sys.stderr)
|
|
236
346
|
return 1
|
|
237
|
-
st, d = _post_json(f"{gw}/board/cards", _card_add_payload(
|
|
347
|
+
st, d = _post_json(f"{gw}/board/cards", _card_add_payload(
|
|
348
|
+
self_name, pid, args.title, body=args.body, ai2=args.ai2,
|
|
349
|
+
priority=args.priority, labels=getattr(args, "labels", None)), token)
|
|
238
350
|
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)
|
|
351
|
+
if args.command == "label":
|
|
352
|
+
# READ the current set before replacing it. If the GET fails we must
|
|
353
|
+
# NOT write — a PATCH built on a failed read would silently CLOBBER
|
|
354
|
+
# every other label on the card. Refuse loudly instead.
|
|
355
|
+
st, cur = _http_get_json(f"{gw}/board/cards/{args.id}", token)
|
|
356
|
+
if st != 200 or not isinstance(cur, dict):
|
|
357
|
+
print(f"swarph board: cannot read card #{args.id} to modify its "
|
|
358
|
+
f"labels (HTTP {st}) — refusing to write, a replacement built "
|
|
359
|
+
f"on a failed read would drop the card's other labels",
|
|
360
|
+
file=sys.stderr)
|
|
361
|
+
return 1
|
|
362
|
+
new = _apply_label(cur.get("labels") or [], args.action, args.label)
|
|
363
|
+
st, d = _patch_json(f"{gw}/board/cards/{args.id}",
|
|
364
|
+
{"actor": self_name, "labels": new}, token)
|
|
365
|
+
return _out(st, d, lambda x: f"card #{x.get('id')} labels -> "
|
|
366
|
+
f"{', '.join(x.get('labels') or []) or '(none)'}", aj)
|
|
239
367
|
if args.command == "move":
|
|
240
368
|
st, d = _patch_json(f"{gw}/board/cards/{args.id}", {"actor": self_name, "stage": args.stage}, token)
|
|
241
369
|
return _out(st, d, lambda x: f"card #{x.get('id')} -> {x.get('stage')}", aj)
|
|
242
370
|
if args.command == "assign":
|
|
243
371
|
st, d = _patch_json(f"{gw}/board/cards/{args.id}", {"actor": self_name, "assignee": args.assignee}, token)
|
|
244
372
|
return _out(st, d, lambda x: f"card #{x.get('id')} assignee -> {x.get('assignee')}", aj)
|
|
373
|
+
if args.command == "thread":
|
|
374
|
+
st, d = _http_get_json(_thread_url(gw, args.id, limit=args.limit), token)
|
|
375
|
+
# NO special-casing of 403/409. `_out` already returns 1 and prints the
|
|
376
|
+
# gateway's `detail` verbatim for every non-2xx, so the refusal reaches
|
|
377
|
+
# the operator intact — a 409 ("predates #181a") and a 403 ("not
|
|
378
|
+
# readable") are never rendered as an empty thread.
|
|
379
|
+
# >>> A HAND-WRITTEN BRANCH HERE WAS DELETED: it re-printed the same
|
|
380
|
+
# detail under a different prefix, carried a comment asserting it
|
|
381
|
+
# prevented a flattening `_out` never did, and a mutation that removed
|
|
382
|
+
# it PASSED ALL 15 TESTS — the definition of a code path doing nothing.
|
|
383
|
+
# Reading it would not have found that; mutating it did. <<<
|
|
384
|
+
return _out(st, d, _format_thread, aj)
|
|
385
|
+
if args.command == "say":
|
|
386
|
+
st, card = _http_get_json(f"{gw}/board/cards/{args.id}", token)
|
|
387
|
+
if st != 200:
|
|
388
|
+
print(f"swarph board cards say: cannot read card #{args.id}: "
|
|
389
|
+
f"{card.get('detail', card)}", file=sys.stderr)
|
|
390
|
+
return 1
|
|
391
|
+
thread_uuid = card.get("thread_uuid")
|
|
392
|
+
if not thread_uuid:
|
|
393
|
+
# Named refusal, not a silent no-op. Same reason the gateway 409s.
|
|
394
|
+
print(f"swarph board cards say: card #{args.id} has no bound thread "
|
|
395
|
+
f"(it predates #181a). Run scripts/migrate_card_threads.py on "
|
|
396
|
+
f"the gateway host.", file=sys.stderr)
|
|
397
|
+
return 1
|
|
398
|
+
try:
|
|
399
|
+
to_node = _thread_recipient(card, args.to_node)
|
|
400
|
+
except RuntimeError as exc:
|
|
401
|
+
print(f"swarph board cards say: {exc}", file=sys.stderr)
|
|
402
|
+
return 1
|
|
403
|
+
st, d = _post_json(
|
|
404
|
+
f"{gw}/messages",
|
|
405
|
+
_card_say_payload(self_name, to_node, args.kind, args.content, thread_uuid),
|
|
406
|
+
token,
|
|
407
|
+
)
|
|
408
|
+
# A 403 here carries the gateway's explanation that attaching PUBLISHES
|
|
409
|
+
# to everyone who can read the card, now and in future, so it needs an
|
|
410
|
+
# explicit `propose` grant. `_out` passes `detail` through whole; it does
|
|
411
|
+
# not need help, and a second copy of that logic is a second thing to
|
|
412
|
+
# keep in sync.
|
|
413
|
+
return _out(st, d, lambda x: f"posted id={x.get('id')} onto card "
|
|
414
|
+
f"#{args.id} (to {to_node})", aj)
|
|
245
415
|
if args.command == "ready":
|
|
246
416
|
st, d = _patch_json(f"{gw}/board/cards/{args.id}", {"actor": self_name, "move_ready": not args.clear}, token)
|
|
247
417
|
return _out(st, d, lambda x: f"card #{x.get('id')} move_ready -> {x.get('move_ready')}", aj)
|
|
@@ -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)
|
|
@@ -128,77 +128,28 @@ def _peer_token_path(self_name: str) -> Path:
|
|
|
128
128
|
return Path.home() / ".config" / "swarph" / f"{self_name}.peer_token"
|
|
129
129
|
|
|
130
130
|
|
|
131
|
-
_TOKEN_KEYS
|
|
131
|
+
# _TOKEN_KEYS moved to swarph_cli.tokens.TOKEN_KEYS (#332). Not re-exported:
|
|
132
|
+
# a second copy of a constant in a second module is precisely how the two token
|
|
133
|
+
# PARSERS diverged in the first place. One definition, one address.
|
|
132
134
|
|
|
133
135
|
|
|
134
136
|
def _read_token_file(path: Path) -> str:
|
|
135
137
|
"""Read a bearer token from a token file OR an env-style file.
|
|
136
138
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
THE IMPLEMENTATION MOVED to `swarph_cli.tokens.read_token_file` (#332).
|
|
140
|
+
droplet's "one parser behind one flag" work (2026-07-26) lived here, in a
|
|
141
|
+
COMMAND module \u2014 so every other verb either reimplemented it or reached
|
|
142
|
+
into a sibling command's private helper. gpt-ops flagged that during the
|
|
143
|
+
#332 review: `onboard` must not depend on `mesh`'s internals. The parser is
|
|
144
|
+
byte-for-byte unchanged; only its address is.
|
|
142
145
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
same flag through onboard._resolve_token, which has always skipped comments
|
|
147
|
-
and parsed KEY=VALUE. Two parsers, one flag -- the same one-thing-two-meanings
|
|
148
|
-
shape as the cursor that meant both observed and woken.
|
|
149
|
-
|
|
150
|
-
Accepts, in order:
|
|
151
|
-
1. an env-style line `MESH_GATEWAY_TOKEN=...` (quotes stripped), or
|
|
152
|
-
2. the first non-comment, non-blank, non-KEY=VALUE line as a bare token.
|
|
146
|
+
This wrapper stays because it is a PATCH POINT: existing suites monkeypatch
|
|
147
|
+
`mesh._read_token_file`, and deleting the name would make those patches
|
|
148
|
+
silently miss (the same trap documented on `_tmux_wake` above).
|
|
153
149
|
"""
|
|
154
|
-
|
|
155
|
-
raw = path.read_text(encoding="utf-8")
|
|
156
|
-
except OSError as exc:
|
|
157
|
-
raise RuntimeError(f"cannot read token file {path}: {exc}") from exc
|
|
158
|
-
|
|
159
|
-
bare: Optional[str] = None
|
|
160
|
-
for lineno, line in enumerate(raw.splitlines(), start=1):
|
|
161
|
-
line = line.strip()
|
|
162
|
-
if not line or line.startswith("#"):
|
|
163
|
-
continue
|
|
164
|
-
if "=" in line:
|
|
165
|
-
key, value = line.split("=", 1)
|
|
166
|
-
if key.strip().upper() in _TOKEN_KEYS:
|
|
167
|
-
return _validated_token(value.strip().strip('"').strip("'"),
|
|
168
|
-
path, lineno)
|
|
169
|
-
continue
|
|
170
|
-
if bare is None:
|
|
171
|
-
bare = line
|
|
172
|
-
bare_lineno = lineno
|
|
173
|
-
if bare is not None:
|
|
174
|
-
return _validated_token(bare, path, bare_lineno)
|
|
175
|
-
raise RuntimeError(
|
|
176
|
-
f"token file {path} contains no token: expected a bare token line or "
|
|
177
|
-
f"one of {', '.join(_TOKEN_KEYS)}=<token>"
|
|
178
|
-
)
|
|
179
|
-
|
|
150
|
+
from swarph_cli.tokens import read_token_file
|
|
180
151
|
|
|
181
|
-
|
|
182
|
-
"""Reject a token that cannot go in an HTTP header, NAMING THE SOURCE.
|
|
183
|
-
|
|
184
|
-
droplet's hardening, and it is worth more than the parse fix: a non-ASCII
|
|
185
|
-
byte in an outbound header surfaced as a latin-1 codec traceback twelve
|
|
186
|
-
frames deep in http/client.py, which points every reader at the HTTP layer
|
|
187
|
-
for what is a malformed config FILE. Fail at the boundary where the operator
|
|
188
|
-
can act, naming the file and the line.
|
|
189
|
-
"""
|
|
190
|
-
try:
|
|
191
|
-
token.encode("latin-1")
|
|
192
|
-
except UnicodeEncodeError as exc:
|
|
193
|
-
bad = token[exc.start:exc.start + 1]
|
|
194
|
-
raise RuntimeError(
|
|
195
|
-
f"token file {path} line {lineno}: token contains a non-ASCII "
|
|
196
|
-
f"character {bad!r} (U+{ord(bad):04X}) at position {exc.start}, "
|
|
197
|
-
f"which cannot be sent in an HTTP Authorization header. "
|
|
198
|
-
f"If this line is a comment, prefix it with '#'; if the file is "
|
|
199
|
-
f"env-style, use MESH_GATEWAY_TOKEN=<token>."
|
|
200
|
-
) from None
|
|
201
|
-
return token
|
|
152
|
+
return read_token_file(path)
|
|
202
153
|
|
|
203
154
|
|
|
204
155
|
def _read_secrets_token(path: Path) -> str:
|
|
@@ -638,6 +589,25 @@ class TmuxSink(Sink):
|
|
|
638
589
|
return f"{count} DM{plural} not yet delivered to {self.name}"
|
|
639
590
|
|
|
640
591
|
|
|
592
|
+
class TmuxNotifySink(Sink):
|
|
593
|
+
"""Show a tmux status-line notice without modifying the pane input buffer."""
|
|
594
|
+
|
|
595
|
+
is_push = True
|
|
596
|
+
|
|
597
|
+
def __init__(self, target: str):
|
|
598
|
+
super().__init__(f"tmux-notify:{target}")
|
|
599
|
+
self.target = target
|
|
600
|
+
|
|
601
|
+
def deliver(self, state: "MonitorState", dms: list, up_to_id: int) -> bool:
|
|
602
|
+
# A notification is deliberately content-free: it must not leak DM text
|
|
603
|
+
# into a shared status line, and unlike TmuxSink it never submits input.
|
|
604
|
+
return _tmux_notify(self.target, max(1, len(dms)))
|
|
605
|
+
|
|
606
|
+
def pending_label(self, count: int) -> str:
|
|
607
|
+
plural = "s" if count != 1 else ""
|
|
608
|
+
return f"{count} DM{plural} not yet notified to {self.name}"
|
|
609
|
+
|
|
610
|
+
|
|
641
611
|
class StdoutSink(Sink):
|
|
642
612
|
"""Write the DM to stdout. Delivery always succeeds."""
|
|
643
613
|
|
|
@@ -672,6 +642,13 @@ def parse_sink(spec: str) -> Sink:
|
|
|
672
642
|
return NoneSink()
|
|
673
643
|
if spec == "stdout":
|
|
674
644
|
return StdoutSink()
|
|
645
|
+
if spec.startswith("tmux-notify:"):
|
|
646
|
+
target = spec[len("tmux-notify:"):]
|
|
647
|
+
if not target:
|
|
648
|
+
raise MonitorSinkError(
|
|
649
|
+
"sink 'tmux-notify:' needs a target, e.g. tmux-notify:lab:0.0"
|
|
650
|
+
)
|
|
651
|
+
return TmuxNotifySink(target)
|
|
675
652
|
if spec.startswith("tmux:"):
|
|
676
653
|
target = spec[len("tmux:"):]
|
|
677
654
|
if not target:
|
|
@@ -688,7 +665,8 @@ def parse_sink(spec: str) -> Sink:
|
|
|
688
665
|
"ignored. Use --deliver pull / tmux:<target> / stdout / none."
|
|
689
666
|
)
|
|
690
667
|
raise MonitorSinkError(
|
|
691
|
-
f"unknown sink {spec!r}; expected pull, none, stdout,
|
|
668
|
+
f"unknown sink {spec!r}; expected pull, none, stdout, tmux:<target>, "
|
|
669
|
+
"or tmux-notify:<target>"
|
|
692
670
|
)
|
|
693
671
|
|
|
694
672
|
|
|
@@ -873,6 +851,24 @@ def _tmux_wake(target: str) -> bool:
|
|
|
873
851
|
return False
|
|
874
852
|
|
|
875
853
|
|
|
854
|
+
def _tmux_notify(target: str, dm_count: int) -> bool:
|
|
855
|
+
"""Show a transient tmux notice without sending any pane keystrokes."""
|
|
856
|
+
plural = "s" if dm_count != 1 else ""
|
|
857
|
+
try:
|
|
858
|
+
subprocess.run(
|
|
859
|
+
["tmux", "display-message", "-t", target,
|
|
860
|
+
f"mesh: {dm_count} new DM{plural}; inbox pending"],
|
|
861
|
+
check=True,
|
|
862
|
+
stdout=subprocess.DEVNULL,
|
|
863
|
+
stderr=subprocess.PIPE,
|
|
864
|
+
text=True,
|
|
865
|
+
)
|
|
866
|
+
return True
|
|
867
|
+
except (OSError, subprocess.CalledProcessError) as exc:
|
|
868
|
+
print(f"[monitor] tmux notification failed: {exc}", file=sys.stderr, flush=True)
|
|
869
|
+
return False
|
|
870
|
+
|
|
871
|
+
|
|
876
872
|
def _select_next_poll_seconds(state: MonitorState) -> int:
|
|
877
873
|
if state.disconnect_since is not None:
|
|
878
874
|
if time.time() - state.disconnect_since > _BACKOFF_5XX_THRESHOLD_SECONDS:
|
|
@@ -56,7 +56,8 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
56
56
|
action="append",
|
|
57
57
|
default=[],
|
|
58
58
|
metavar="SINK",
|
|
59
|
-
help="repeatable; pull (default) | tmux:<target> |
|
|
59
|
+
help="repeatable; pull (default) | tmux:<target> | tmux-notify:<target> "
|
|
60
|
+
"| stdout | none. "
|
|
60
61
|
"Each sink gets its OWN delivery ledger.",
|
|
61
62
|
)
|
|
62
63
|
start.add_argument("--poll-s", type=int, default=mesh._DEFAULT_POLL_S)
|