swarph-cli 0.40.3__tar.gz → 0.41.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {swarph_cli-0.40.3/src/swarph_cli.egg-info → swarph_cli-0.41.1}/PKG-INFO +34 -7
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/README.md +33 -5
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/pyproject.toml +8 -2
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/__init__.py +1 -1
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/codegraph.py +20 -1
- swarph_cli-0.41.1/src/swarph_cli/commands/codegraph_hook.py +272 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/hooks.py +36 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/mesh.py +82 -5
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/gateway/server.py +15 -1
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/main.py +1 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1/src/swarph_cli.egg-info}/PKG-INFO +34 -7
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli.egg-info/SOURCES.txt +4 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_brain_ask_command.py +1 -1
- swarph_cli-0.41.1/tests/test_codegraph_caller_scope.py +103 -0
- swarph_cli-0.41.1/tests/test_codegraph_hook.py +193 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_gateway_brain.py +33 -0
- swarph_cli-0.41.1/tests/test_process_liveness.py +97 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_watchdog.py +1 -1
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/LICENSE +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/setup.cfg +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/bench/__init__.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/bench/backends.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/bench/pack.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/bench/prices.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/bench/quality.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/bench/refresh_prices.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/bench/runner.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/bench/validate.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/caller.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/capture/__init__.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/capture/harden.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/capture/lineage.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/capture/liveness.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/capture/manifest.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/capture/paths.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/capture/verify.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/cell.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/chain_token.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/__init__.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/_display.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/_gateway_client.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/add.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/bench.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/board.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/board_merge_check.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/brain.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/brain_ask.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/cell.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/cell_selfcheck.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/channel.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/chat.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/compress.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/daemon.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/event.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/gateway.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/group.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/highlight.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/hook_output.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/import_session.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/init.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/install_hook.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/install_multiplexer.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/lane.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/mcp_server.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/memory.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/memory_sync.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/monitor.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/okf_links.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/onboard.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/protocol_handler.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/ratify.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/schedule.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/security.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/service.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/spawn.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/timeline.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/commands/watchdog.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/compress/__init__.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/compress/levers.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/compress/marker.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/compress/verify.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/console_safe.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/delivery_queue.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/gateway/__init__.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/gateway/feature_registry.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/gateway/lanes_control.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/gateway/schema.sql +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/gateway/services_control.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/multiplexer.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/parsers/__init__.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/parsers/claude.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/scripts/ensure_monitor.sh +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/service/__init__.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/service/app.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/service/providers.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/session_bridge.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/stall_alert.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli.egg-info/entry_points.txt +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli.egg-info/requires.txt +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/src/swarph_cli.egg-info/top_level.txt +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_add_security_gate.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_artifact_add.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_artifact_hash.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_artifact_lib.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_artifact_mcp.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_artifact_skill.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_artifact_tool.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_artifact_uri.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_bench_backends.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_bench_command.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_bench_pack.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_bench_prices.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_bench_quality.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_bench_runner.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_bench_validate.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_board_command.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_board_merge_check.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_brain_ask_env_diagnosis.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_brain_command.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_caller_meta_guard.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_capture_lineage.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_capture_liveness.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_capture_manifest.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_capture_paths.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_capture_security.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_cell_command_dispatch.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_cell_harden.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_cell_loader.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_cell_selfcheck.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_cell_verify.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_chain_token.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_channel_command.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_channel_post.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_chat_command.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_claude_parser.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_claude_tmux_template.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_codegraph_command.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_codegraph_mcp.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_compress_caller_convention.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_compress_command.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_compress_levers.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_compress_marker.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_compress_verify.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_console_encoding_e2e.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_daemon_command.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_daemon_console_encoding.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_daemon_delivery.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_delivery_queue.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_display_sanitize.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_event_emit.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_feature_to_uri.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_gateway_client.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_gateway_command.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_group_command.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_highlight_command.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_hook_output.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_hooks_activity_marker.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_hooks_add.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_hooks_bundle.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_hooks_init.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_hooks_lifecycle.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_hooks_merge.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_import_command.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_init_command.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_install_hook.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_install_multiplexer_command.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_lane_command.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_main.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_main_unknown_verb_guard.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_mcp_server.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_memory_command.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_memory_gateway.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_memory_sync.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_memory_sync_membrane.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_mesh_command.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_mesh_inbox_mark_read.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_mesh_inbox_sanitize.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_mesh_sidecar.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_mesh_sidecar_cursor_decoupled.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_mesh_sidecar_wake_survives_read.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_meta_edge_identity.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_monitor_cli.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_monitor_daemon_collision.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_monitor_self_name_verification.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_monitor_state_split.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_multiplexer.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_okf_links.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_onboard_command.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_packaged_artifacts_exist.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_protocol_handler.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_providers_isolation.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_ratify_command.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_repl_caller_convention.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_schedule_command.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_schedule_create_contract.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_security.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_service_app.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_service_command.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_service_providers.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_session_bridge_inject.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_session_bridge_probe.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_session_bridge_resolve.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_session_path_role_gate.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_shipped_units_match_the_cli.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_smoke_chat.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_smoke_one_shot.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_smoke_phase_5_5.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_spawn_base_hoist.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_spawn_command.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_spawn_live_pin.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_spawn_mitosis_lineage.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_spawn_tmux_session.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_spawn_windows_relaunch.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_stall_alert.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_timeline_command.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_token_file_one_parser.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_version_consistency.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_watchdog_dm_wake.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_watchdog_dm_wake_wiring.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_watchdog_emit_health.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_watchdog_install_flags.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_watchdog_model_rung.py +0 -0
- {swarph_cli-0.40.3 → swarph_cli-0.41.1}/tests/test_watchdog_stale_peers.py +0 -0
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: swarph-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.41.1
|
|
4
4
|
Summary: The `swarph` binary — a multi-LLM CLI and mesh-gateway client + bundled server (`swarph gateway`): one-shot prompts across providers, interactive `chat`, multi-provider `spawn` (claude/codex/antigravity via a ProviderMembrane with subprocess billing-scrub), `mesh` send/inbox/register, `brain-ask` over the swarph-brain (gbrain) semantic memory, git-backed `assisted_memory`, session `import`, and a stranded-session `watchdog`.
|
|
5
5
|
Author: Pierre Samson, Claude Opus
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/BrainSurfing-tech/swarph-cli
|
|
8
8
|
Project-URL: Source, https://github.com/BrainSurfing-tech/swarph-cli
|
|
9
9
|
Project-URL: Substrate, https://github.com/BrainSurfing-tech/swarph-mesh
|
|
10
|
-
Project-URL: Spec, https://github.com/darw007d/hedge-fund-mcp/blob/main/research/swarph_cli/PLAN.md
|
|
11
10
|
Keywords: swarph,llm,cli,mesh,gemini,claude,deepseek
|
|
12
11
|
Classifier: Development Status :: 3 - Alpha
|
|
13
12
|
Classifier: Environment :: Console
|
|
@@ -467,13 +466,41 @@ swarph watchdog --check --peer researcher --gateway http://localhost:8788 --dm-w
|
|
|
467
466
|
Installs Claude Code hooks as **content** wired into `~/.claude/settings.json` — a hook becomes an installable artifact (a script + its event/matcher bindings merged into your settings) with no swarph-cli version bump per hook, the same way `watchdog --install-service` ships systemd units as bundled data.
|
|
468
467
|
|
|
469
468
|
```bash
|
|
470
|
-
swarph hooks init
|
|
471
|
-
swarph hooks add
|
|
472
|
-
swarph hooks add
|
|
473
|
-
swarph hooks
|
|
469
|
+
swarph hooks init # install the recommended bundled set
|
|
470
|
+
swarph hooks add codegraph-on-grep # structural search alongside grep (see below)
|
|
471
|
+
swarph hooks add cell-resilience # install one builtin by name
|
|
472
|
+
swarph hooks add ./my-hook # install a local bundle dir (hook.json + script)
|
|
473
|
+
swarph hooks list # builtins + install status (installed|available)
|
|
474
474
|
swarph hooks remove cell-resilience
|
|
475
475
|
```
|
|
476
476
|
|
|
477
|
+
**Bundled `codegraph-on-grep`** *(0.41.0)*. Binds `PostToolUse`/`Bash`. When a `grep`/`rg` searches
|
|
478
|
+
**code**, it also queries the mesh gateway's structural codegraph and hands back the symbol-level
|
|
479
|
+
answer — definitions, `file:line`, and **caller counts, which grep structurally cannot see**:
|
|
480
|
+
|
|
481
|
+
```
|
|
482
|
+
CODEGRAPH (structural, index 2.1h old) for 'def _board_grant' — grep found text; this is
|
|
483
|
+
the symbol graph, incl. CALLER COUNTS grep cannot see:
|
|
484
|
+
mesh-gateway/server.py:1352 function _board_grant callers=11
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
The point is **context cost**: a grep returning 200 matching lines is far more expensive than the
|
|
488
|
+
handful of symbols that answer *"where is this defined and who calls it"*.
|
|
489
|
+
|
|
490
|
+
- **It supplements grep; it never blocks it.** grep stays correct for config, logs and string
|
|
491
|
+
literals — the codegraph indexes *symbols*.
|
|
492
|
+
- **An unavailable index is reported loudly, never as "no matches."** A failed lookup rendered as a
|
|
493
|
+
real negative teaches exactly the wrong thing, so the hook says so and marks the answer
|
|
494
|
+
unverified. A genuinely empty result from a *working* index says `a REAL negative` explicitly, and
|
|
495
|
+
a stale index reports its age.
|
|
496
|
+
- **No local index required.** It asks the gateway, which scopes both rows and caller counts to your
|
|
497
|
+
visibility — resolved from your **bearer token**, never from a field you send, so it cannot be
|
|
498
|
+
self-asserted.
|
|
499
|
+
|
|
500
|
+
Needs `SWARPH_SELF` (or `--as`), your peer token at `~/.config/swarph/<cell>.peer_token`, and a
|
|
501
|
+
reachable gateway. Do not run a hand-rolled copy alongside it: two hooks per `Bash` call means two
|
|
502
|
+
lookups, doubling the cost the hook exists to cut.
|
|
503
|
+
|
|
477
504
|
**Trust model.** Three tiers: `builtin` (trusted, bundled with swarph-cli — installs without a prompt), `local` (a bundle dir you point at — shown then confirmed before any write), and `published`/`@cell/name` (**fails closed in v1** — never installs another cell's unreviewed code). Signed-publisher identity plus a publish-time security gate is the v2 model.
|
|
478
505
|
|
|
479
506
|
**Bundled `cell-resilience`.** Binds `StopFailure`/`rate_limit` + `Stop`/`(all)` to a script that writes `$XDG_STATE_HOME/swarph/idle_since.json` (`{"session","reason","hook_event","ts"}`, `reason=throttle|normal`) — the push-side throttle detector the watchdog's `--dm-wake` can read instead of polling. Observational only: it never blocks the session and always exits 0 (jq if present, printf/sed fallback otherwise).
|
|
@@ -655,7 +682,7 @@ Dry-run is the default — nothing mutates without `--apply`. Cron-friendly exit
|
|
|
655
682
|
| `8` | refused — not idempotent (second pass kept cutting; signal-eating alarm) |
|
|
656
683
|
|
|
657
684
|
Design spec: `docs/superpowers/specs/2026-06-11-swarph-context-compressor-design.md`
|
|
658
|
-
(in
|
|
685
|
+
(in a private design repo; not published).
|
|
659
686
|
|
|
660
687
|
### `--json` mode semantics
|
|
661
688
|
|
|
@@ -420,13 +420,41 @@ swarph watchdog --check --peer researcher --gateway http://localhost:8788 --dm-w
|
|
|
420
420
|
Installs Claude Code hooks as **content** wired into `~/.claude/settings.json` — a hook becomes an installable artifact (a script + its event/matcher bindings merged into your settings) with no swarph-cli version bump per hook, the same way `watchdog --install-service` ships systemd units as bundled data.
|
|
421
421
|
|
|
422
422
|
```bash
|
|
423
|
-
swarph hooks init
|
|
424
|
-
swarph hooks add
|
|
425
|
-
swarph hooks add
|
|
426
|
-
swarph hooks
|
|
423
|
+
swarph hooks init # install the recommended bundled set
|
|
424
|
+
swarph hooks add codegraph-on-grep # structural search alongside grep (see below)
|
|
425
|
+
swarph hooks add cell-resilience # install one builtin by name
|
|
426
|
+
swarph hooks add ./my-hook # install a local bundle dir (hook.json + script)
|
|
427
|
+
swarph hooks list # builtins + install status (installed|available)
|
|
427
428
|
swarph hooks remove cell-resilience
|
|
428
429
|
```
|
|
429
430
|
|
|
431
|
+
**Bundled `codegraph-on-grep`** *(0.41.0)*. Binds `PostToolUse`/`Bash`. When a `grep`/`rg` searches
|
|
432
|
+
**code**, it also queries the mesh gateway's structural codegraph and hands back the symbol-level
|
|
433
|
+
answer — definitions, `file:line`, and **caller counts, which grep structurally cannot see**:
|
|
434
|
+
|
|
435
|
+
```
|
|
436
|
+
CODEGRAPH (structural, index 2.1h old) for 'def _board_grant' — grep found text; this is
|
|
437
|
+
the symbol graph, incl. CALLER COUNTS grep cannot see:
|
|
438
|
+
mesh-gateway/server.py:1352 function _board_grant callers=11
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
The point is **context cost**: a grep returning 200 matching lines is far more expensive than the
|
|
442
|
+
handful of symbols that answer *"where is this defined and who calls it"*.
|
|
443
|
+
|
|
444
|
+
- **It supplements grep; it never blocks it.** grep stays correct for config, logs and string
|
|
445
|
+
literals — the codegraph indexes *symbols*.
|
|
446
|
+
- **An unavailable index is reported loudly, never as "no matches."** A failed lookup rendered as a
|
|
447
|
+
real negative teaches exactly the wrong thing, so the hook says so and marks the answer
|
|
448
|
+
unverified. A genuinely empty result from a *working* index says `a REAL negative` explicitly, and
|
|
449
|
+
a stale index reports its age.
|
|
450
|
+
- **No local index required.** It asks the gateway, which scopes both rows and caller counts to your
|
|
451
|
+
visibility — resolved from your **bearer token**, never from a field you send, so it cannot be
|
|
452
|
+
self-asserted.
|
|
453
|
+
|
|
454
|
+
Needs `SWARPH_SELF` (or `--as`), your peer token at `~/.config/swarph/<cell>.peer_token`, and a
|
|
455
|
+
reachable gateway. Do not run a hand-rolled copy alongside it: two hooks per `Bash` call means two
|
|
456
|
+
lookups, doubling the cost the hook exists to cut.
|
|
457
|
+
|
|
430
458
|
**Trust model.** Three tiers: `builtin` (trusted, bundled with swarph-cli — installs without a prompt), `local` (a bundle dir you point at — shown then confirmed before any write), and `published`/`@cell/name` (**fails closed in v1** — never installs another cell's unreviewed code). Signed-publisher identity plus a publish-time security gate is the v2 model.
|
|
431
459
|
|
|
432
460
|
**Bundled `cell-resilience`.** Binds `StopFailure`/`rate_limit` + `Stop`/`(all)` to a script that writes `$XDG_STATE_HOME/swarph/idle_since.json` (`{"session","reason","hook_event","ts"}`, `reason=throttle|normal`) — the push-side throttle detector the watchdog's `--dm-wake` can read instead of polling. Observational only: it never blocks the session and always exits 0 (jq if present, printf/sed fallback otherwise).
|
|
@@ -608,7 +636,7 @@ Dry-run is the default — nothing mutates without `--apply`. Cron-friendly exit
|
|
|
608
636
|
| `8` | refused — not idempotent (second pass kept cutting; signal-eating alarm) |
|
|
609
637
|
|
|
610
638
|
Design spec: `docs/superpowers/specs/2026-06-11-swarph-context-compressor-design.md`
|
|
611
|
-
(in
|
|
639
|
+
(in a private design repo; not published).
|
|
612
640
|
|
|
613
641
|
### `--json` mode semantics
|
|
614
642
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "swarph-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.41.1"
|
|
8
8
|
description = "The `swarph` binary — a multi-LLM CLI and mesh-gateway client + bundled server (`swarph gateway`): one-shot prompts across providers, interactive `chat`, multi-provider `spawn` (claude/codex/antigravity via a ProviderMembrane with subprocess billing-scrub), `mesh` send/inbox/register, `brain-ask` over the swarph-brain (gbrain) semantic memory, git-backed `assisted_memory`, session `import`, and a stranded-session `watchdog`."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -51,7 +51,6 @@ dependencies = [
|
|
|
51
51
|
Homepage = "https://github.com/BrainSurfing-tech/swarph-cli"
|
|
52
52
|
Source = "https://github.com/BrainSurfing-tech/swarph-cli"
|
|
53
53
|
Substrate = "https://github.com/BrainSurfing-tech/swarph-mesh"
|
|
54
|
-
Spec = "https://github.com/darw007d/hedge-fund-mcp/blob/main/research/swarph_cli/PLAN.md"
|
|
55
54
|
|
|
56
55
|
[project.optional-dependencies]
|
|
57
56
|
dev = ["pytest>=7.0"]
|
|
@@ -111,4 +110,11 @@ swarph_cli = ["systemd/*.service", "systemd/*.timer", "systemd/*.default",
|
|
|
111
110
|
|
|
112
111
|
[tool.pytest.ini_options]
|
|
113
112
|
testpaths = ["tests"]
|
|
113
|
+
# The tree must win over any installed copy. Without this, `pip install swarph-cli`
|
|
114
|
+
# on the same box shadows src/ and the suite silently tests the PUBLISHED wheel
|
|
115
|
+
# instead of your edits — every local change reads green whether or not it works.
|
|
116
|
+
# Measured 2026-08-01: 1689 passed against site-packages vs 1690 against the tree;
|
|
117
|
+
# the only test that noticed was a brand-new one, because the tree and 0.41.0 were
|
|
118
|
+
# nearly identical. That agreement is a polluted baseline, not isolation.
|
|
119
|
+
pythonpath = ["src"]
|
|
114
120
|
addopts = "-v --tb=short"
|
|
@@ -145,8 +145,27 @@ def structural_query(term, *, index_path, caller_cell, limit=8, allowlist=None)
|
|
|
145
145
|
# verbatim edge kinds (contains/imports/decorates/references/calls/...), and counting
|
|
146
146
|
# all of them would pollute the blast-radius number. 'calls' (plural) is CodeGraph's
|
|
147
147
|
# actual call-edge kind.
|
|
148
|
+
#
|
|
149
|
+
# >>> CARD #194 — THE COUNT IS SCOPED TO THE CALLER-VISIBLE REPOS, exactly
|
|
150
|
+
# like the rows above. <<< This was an unscoped COUNT(*) while `s.repo IN
|
|
151
|
+
# (allowed)` gated the rows, so a caller who could NOT see a private repo
|
|
152
|
+
# still received a caller count that INCLUDED its callers.
|
|
153
|
+
#
|
|
154
|
+
# drop-on-meta-edge, seat-A: "a count that sums callers living in a private
|
|
155
|
+
# repo LEAKS THE EXISTENCE AND SIZE of those private callers — the exact
|
|
156
|
+
# payload #44's transport refuses, delivered as an integer instead of a
|
|
157
|
+
# path." Measured 2026-07-31: cross-visibility `calls` edges were 0 on the
|
|
158
|
+
# live index, so the leak was LATENT — but this is the blast-radius number
|
|
159
|
+
# the feature exists to serve, and the gateway proxy would have exposed it
|
|
160
|
+
# to every remote peer.
|
|
161
|
+
#
|
|
162
|
+
# The unifying rule: EVERY OBSERVABLE — rows, counts, scores, freshness —
|
|
163
|
+
# must be a function of only the caller-visible subset, never the full
|
|
164
|
+
# index. Gating the rows is ONE INSTANCE of it, not the whole of it.
|
|
148
165
|
callers = con.execute(
|
|
149
|
-
"SELECT COUNT(*) FROM edges
|
|
166
|
+
f"SELECT COUNT(*) FROM edges e JOIN symbols src ON src.id = e.src_symbol "
|
|
167
|
+
f"WHERE e.dst_symbol=? AND e.edge_type='calls' AND src.repo IN ({qmarks})",
|
|
168
|
+
(sid, *sorted(allowed))
|
|
150
169
|
).fetchone()[0]
|
|
151
170
|
out.append({"repo": repo, "name": name, "kind": kind,
|
|
152
171
|
"file_path": fp, "start_line": line, "callers": callers, "score": score,
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""``swarph codegraph-hook`` — PostToolUse(Bash) structural-search companion.
|
|
2
|
+
|
|
3
|
+
WHEN A grep/rg SEARCHES CODE, ALSO ASK THE CODEGRAPH, and hand the structural
|
|
4
|
+
answer back as context. Card #194.
|
|
5
|
+
|
|
6
|
+
WHY IT EXISTS. Agents reach for grep where the symbol graph is the right
|
|
7
|
+
instrument — lab-ovh was corrected three times in one session for exactly that,
|
|
8
|
+
and the third correction landed INSIDE the fix for the first two. Instructions
|
|
9
|
+
demonstrably do not hold at the moment of typing, so this is mechanical rather
|
|
10
|
+
than a reminder. The token argument is the commander's: a grep that returns 200
|
|
11
|
+
matching lines costs far more context than the six symbols that actually answer
|
|
12
|
+
"where is this defined and who calls it".
|
|
13
|
+
|
|
14
|
+
>>> IT SUPPLEMENTS, IT NEVER BLOCKS. <<< grep is genuinely correct for config,
|
|
15
|
+
logs, /etc and string literals; the codegraph indexes SYMBOLS. The failure this
|
|
16
|
+
guards is "I only ever saw grep's answer", not "grep was used".
|
|
17
|
+
|
|
18
|
+
>>> AND IT MUST NEVER DEGRADE SILENTLY. <<< A missing index makes a structural
|
|
19
|
+
query return `[]` — indistinguishable from a real negative, which is the exact
|
|
20
|
+
shape of the (gbrain unreachable) incident and of card #200's drain. So an
|
|
21
|
+
unavailable backend is reported LOUDLY as an incident, never as "no matches".
|
|
22
|
+
|
|
23
|
+
PEER MODE. Unlike lab-ovh's original local hook, a peer has no local index — it
|
|
24
|
+
asks the gateway's ``POST /codegraph`` proxy, which applies the A8 visibility
|
|
25
|
+
gate to rows AND to caller counts (swarph-cli PR #165). The caller identity comes
|
|
26
|
+
from the bearer token, never from a field, so it cannot be self-asserted.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
import os
|
|
33
|
+
import re
|
|
34
|
+
import sys
|
|
35
|
+
import urllib.error
|
|
36
|
+
import urllib.request
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import Optional
|
|
39
|
+
|
|
40
|
+
DEFAULT_GATEWAY = os.environ.get("MESH_GATEWAY_URL", "http://localhost:8788")
|
|
41
|
+
TIMEOUT_S = 6
|
|
42
|
+
MAX_ROWS = 6
|
|
43
|
+
|
|
44
|
+
# ── trigger heuristics ────────────────────────────────────────────────────
|
|
45
|
+
# Ported from the hook that ran on lab-ovh for a day. Every one of these three
|
|
46
|
+
# patterns exists because a LOOSER version misfired in production.
|
|
47
|
+
|
|
48
|
+
# 1. grep/rg must be at COMMAND POSITION — line start, or right after a pipe /
|
|
49
|
+
# ; / && / $( . Matching "grep" after arbitrary whitespace fires on any command
|
|
50
|
+
# whose text merely MENTIONS the word, including a heredoc body: the first live
|
|
51
|
+
# firing did exactly that and extracted the heredoc delimiter as the search term.
|
|
52
|
+
_AT_COMMAND_POS = re.compile(r"(?:^|[|;&]|\$\()\s*(?:sudo\s+)?(?:grep|rg)\s")
|
|
53
|
+
|
|
54
|
+
# 2. A heredoc means most of the command is DATA, not shell — too noisy to parse.
|
|
55
|
+
_HEREDOC = re.compile(r"<<-?['\"]?\w*(?:EOF|PY)")
|
|
56
|
+
|
|
57
|
+
# 3. It should look like a CODE search, not a config/log one.
|
|
58
|
+
_CODEISH = re.compile(
|
|
59
|
+
r"\.(?:py|ts|tsx|js|jsx|go|rs|java|rb|c|h|cpp)\b|--include|(?:^|\s)-r\s|src/"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _looks_like_code_search(cmd: str) -> bool:
|
|
64
|
+
if not _AT_COMMAND_POS.search(cmd):
|
|
65
|
+
return False
|
|
66
|
+
if _HEREDOC.search(cmd):
|
|
67
|
+
return False
|
|
68
|
+
return bool(_CODEISH.search(cmd))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def extract_term(cmd: str) -> Optional[str]:
|
|
72
|
+
"""The search term ADJACENT to grep — not the first quoted string anywhere.
|
|
73
|
+
|
|
74
|
+
A `gh pr close --comment "...'dead'..." && grep foo bar` fired a query for
|
|
75
|
+
'dead', lifted out of unrelated prose. So the scan starts AT the grep token.
|
|
76
|
+
"""
|
|
77
|
+
m = _AT_COMMAND_POS.search(cmd)
|
|
78
|
+
if not m:
|
|
79
|
+
return None
|
|
80
|
+
tail = cmd[m.end():]
|
|
81
|
+
# Skip flags (-n, -R, --include=*.py, …) to reach the pattern itself.
|
|
82
|
+
for tok in _tokenize(tail):
|
|
83
|
+
if tok.startswith("-"):
|
|
84
|
+
continue
|
|
85
|
+
term = tok.strip("'\"")
|
|
86
|
+
# A regex-metachar soup is not a useful symbol query.
|
|
87
|
+
cleaned = re.sub(r"[^\w\s.]", " ", term).strip()
|
|
88
|
+
return cleaned or None
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _tokenize(s: str) -> list:
|
|
93
|
+
"""Shell-ish split that keeps quoted strings together.
|
|
94
|
+
|
|
95
|
+
NOTE: no early break. An earlier version stopped after the FIRST completed
|
|
96
|
+
token, so a command whose first argument is a flag (`grep -rn 'pat' src/`)
|
|
97
|
+
yielded only `-rn`, the caller skipped it as a flag, and extraction returned
|
|
98
|
+
None — the hook silently never fired on the most common grep shape there is.
|
|
99
|
+
Caught by test_skips_flags_to_reach_the_pattern.
|
|
100
|
+
"""
|
|
101
|
+
out, cur, quote, started = [], "", None, False
|
|
102
|
+
for ch in s:
|
|
103
|
+
if quote:
|
|
104
|
+
if ch == quote:
|
|
105
|
+
quote = None
|
|
106
|
+
else:
|
|
107
|
+
cur += ch
|
|
108
|
+
elif ch in "'\"":
|
|
109
|
+
quote, started = ch, True
|
|
110
|
+
elif ch.isspace():
|
|
111
|
+
if cur or started:
|
|
112
|
+
out.append(cur)
|
|
113
|
+
cur, started = "", False
|
|
114
|
+
else:
|
|
115
|
+
cur += ch
|
|
116
|
+
started = True
|
|
117
|
+
if cur or started:
|
|
118
|
+
out.append(cur)
|
|
119
|
+
return out
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ── gateway query ─────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
def _token_path(self_name: str) -> Path:
|
|
125
|
+
return Path.home() / ".config" / "swarph" / f"{self_name}.peer_token"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def query_gateway(term: str, gateway: str, token: str, limit: int = MAX_ROWS) -> dict:
|
|
129
|
+
"""POST /codegraph. Returns the envelope, or {"error": "..."} — NEVER {}.
|
|
130
|
+
|
|
131
|
+
>>> AN UNREACHABLE OR REFUSING BACKEND IS AN ERROR, NOT AN EMPTY RESULT. <<<
|
|
132
|
+
Collapsing them is the defect this hook exists to avoid teaching.
|
|
133
|
+
"""
|
|
134
|
+
req = urllib.request.Request(
|
|
135
|
+
f"{gateway.rstrip('/')}/codegraph",
|
|
136
|
+
data=json.dumps({"query": term, "limit": limit}).encode(),
|
|
137
|
+
headers={"Authorization": f"Bearer {token}",
|
|
138
|
+
"Content-Type": "application/json"},
|
|
139
|
+
method="POST",
|
|
140
|
+
)
|
|
141
|
+
try:
|
|
142
|
+
with urllib.request.urlopen(req, timeout=TIMEOUT_S) as r:
|
|
143
|
+
return json.loads(r.read().decode())
|
|
144
|
+
except urllib.error.HTTPError as e:
|
|
145
|
+
detail = ""
|
|
146
|
+
try:
|
|
147
|
+
detail = json.loads(e.read().decode()).get("detail", "")
|
|
148
|
+
except Exception: # noqa: BLE001
|
|
149
|
+
pass
|
|
150
|
+
return {"error": f"HTTP {e.code}: {detail or e.reason}"}
|
|
151
|
+
except Exception as e: # noqa: BLE001 — transport/parse
|
|
152
|
+
return {"error": f"{type(e).__name__}: {e}"}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _match_quality(term: str, rows: list) -> tuple:
|
|
156
|
+
"""Did the results actually match what was ASKED, or only a common token?
|
|
157
|
+
|
|
158
|
+
>>> THE EMPTY ANSWER WAS MADE HONEST; THE NON-EMPTY ONE WAS NOT. <<<
|
|
159
|
+
(Reported first-hand by a peer, 2026-08-01.) The index's query sanitiser
|
|
160
|
+
OR-JOINS tokens, so `def command_beta_executor` matches anything containing
|
|
161
|
+
"command". He grepped a private-repo file and got six confident-looking
|
|
162
|
+
swarph-cli symbols WITH CALLER COUNTS — plausible structure from the wrong
|
|
163
|
+
repository, and caller counts make it read authoritative. His words: "the
|
|
164
|
+
failure mode you engineered out of the empty case walked back in through the
|
|
165
|
+
non-empty one."
|
|
166
|
+
|
|
167
|
+
So: find the query's DISTINCTIVE token — the longest content token, which is
|
|
168
|
+
the one carrying the intent (`command_beta_executor`, not `def`) — and check
|
|
169
|
+
whether ANY returned symbol name actually contains it. If none does, every
|
|
170
|
+
row is a common-token coincidence and must be labelled as such rather than
|
|
171
|
+
served as an answer.
|
|
172
|
+
|
|
173
|
+
Returns (distinctive_token, n_rows_matching_it).
|
|
174
|
+
"""
|
|
175
|
+
toks = [t for t in re.findall(r"[A-Za-z0-9_]+", (term or "").lower())
|
|
176
|
+
if len(t) > 2 and t not in {"def", "class", "async", "the", "for"}]
|
|
177
|
+
if not toks:
|
|
178
|
+
return ("", len(rows))
|
|
179
|
+
key = max(toks, key=len)
|
|
180
|
+
hits = sum(1 for r in rows if key in str(r.get("name", "")).lower())
|
|
181
|
+
return (key, hits)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def render(term: str, env: dict) -> str:
|
|
185
|
+
"""Format the envelope for injection. Errors are LOUD and named."""
|
|
186
|
+
if "error" in env:
|
|
187
|
+
return (f"CODEGRAPH UNAVAILABLE for '{term}' — {env['error']}\n"
|
|
188
|
+
f" >>> THIS IS NOT 'no matches'. The structural index could not be "
|
|
189
|
+
f"consulted, so grep's answer is the ONLY answer you have. Treat "
|
|
190
|
+
f"definitions/callers as UNVERIFIED.")
|
|
191
|
+
|
|
192
|
+
rows = env.get("results") or []
|
|
193
|
+
fresh = env.get("freshness") or []
|
|
194
|
+
stale = [f for f in fresh if f.get("stale")]
|
|
195
|
+
age = ""
|
|
196
|
+
if fresh:
|
|
197
|
+
hrs = max((f.get("index_age_hours") or 0) for f in fresh)
|
|
198
|
+
age = f", index {hrs:.1f}h old"
|
|
199
|
+
if stale:
|
|
200
|
+
age += " ⚠ STALE — verify line numbers against the file"
|
|
201
|
+
|
|
202
|
+
if not rows:
|
|
203
|
+
body = (" no structural matches (the index IS present and answered — "
|
|
204
|
+
"a REAL negative)")
|
|
205
|
+
else:
|
|
206
|
+
body = "\n".join(
|
|
207
|
+
f" {r.get('repo')}/{r.get('file_path')}:{r.get('start_line')} "
|
|
208
|
+
f"{r.get('kind')} {r.get('name')} callers={r.get('callers')}"
|
|
209
|
+
for r in rows[:MAX_ROWS]
|
|
210
|
+
)
|
|
211
|
+
warn = ""
|
|
212
|
+
if rows:
|
|
213
|
+
key, hits = _match_quality(term, rows)
|
|
214
|
+
if key and hits == 0:
|
|
215
|
+
repos = sorted({str(r.get("repo")) for r in rows})
|
|
216
|
+
warn = (f"\n >>> FUZZY MATCH — NOT AN ANSWER TO YOUR QUERY. No returned symbol's "
|
|
217
|
+
f"name contains {key!r}. These matched a COMMON TOKEN only"
|
|
218
|
+
+ (f", and all are from: {', '.join(repos)}" if repos else "")
|
|
219
|
+
+ f". Treat them as unrelated: the symbol you grepped for is NOT in "
|
|
220
|
+
f"what this index can see. <<<")
|
|
221
|
+
return (f"CODEGRAPH (structural{age}) for '{term}' — grep found text; this is "
|
|
222
|
+
f"the symbol graph, incl. CALLER COUNTS grep cannot see:\n{body}{warn}\n\n"
|
|
223
|
+
f"Use this for definitions/callers/blast-radius. grep remains correct "
|
|
224
|
+
f"for config, logs and string literals the codegraph does not index.")
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def run_codegraph_hook(argv: Optional[list] = None) -> int:
|
|
228
|
+
"""PostToolUse(Bash) entry point. ALWAYS exits 0 — it must never fail a turn."""
|
|
229
|
+
argv = list(argv or [])
|
|
230
|
+
self_name = os.environ.get("SWARPH_SELF", "").strip()
|
|
231
|
+
gateway = DEFAULT_GATEWAY
|
|
232
|
+
for i, a in enumerate(argv):
|
|
233
|
+
if a == "--as" and i + 1 < len(argv):
|
|
234
|
+
self_name = argv[i + 1]
|
|
235
|
+
elif a == "--gateway" and i + 1 < len(argv):
|
|
236
|
+
gateway = argv[i + 1]
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
payload = json.loads(sys.stdin.read() or "{}")
|
|
240
|
+
except Exception: # noqa: BLE001
|
|
241
|
+
return 0
|
|
242
|
+
cmd = ((payload.get("tool_input") or {}).get("command") or "").strip()
|
|
243
|
+
if not cmd or not _looks_like_code_search(cmd):
|
|
244
|
+
return 0
|
|
245
|
+
term = extract_term(cmd)
|
|
246
|
+
if not term:
|
|
247
|
+
return 0
|
|
248
|
+
|
|
249
|
+
if not self_name:
|
|
250
|
+
_emit(f"CODEGRAPH SKIPPED for '{term}' — no cell identity (set SWARPH_SELF "
|
|
251
|
+
f"or pass --as). Not a negative result: the graph was never asked.")
|
|
252
|
+
return 0
|
|
253
|
+
tp = _token_path(self_name)
|
|
254
|
+
try:
|
|
255
|
+
token = tp.read_text().strip()
|
|
256
|
+
except OSError as e:
|
|
257
|
+
_emit(f"CODEGRAPH UNAVAILABLE for '{term}' — peer token unreadable at {tp}: "
|
|
258
|
+
f"{e}. Not a negative result: the graph was never asked.")
|
|
259
|
+
return 0
|
|
260
|
+
|
|
261
|
+
_emit(render(term, query_gateway(term, gateway, token)))
|
|
262
|
+
return 0
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _emit(text: str) -> None:
|
|
266
|
+
"""Hand context back to the model via the documented hook JSON shape."""
|
|
267
|
+
print(json.dumps({
|
|
268
|
+
"hookSpecificOutput": {
|
|
269
|
+
"hookEventName": "PostToolUse",
|
|
270
|
+
"additionalContext": text,
|
|
271
|
+
}
|
|
272
|
+
}))
|
|
@@ -155,7 +155,43 @@ exit 0
|
|
|
155
155
|
"""
|
|
156
156
|
|
|
157
157
|
|
|
158
|
+
# The bundled codegraph-on-grep script (card #194). Deliberately a THIN WRAPPER
|
|
159
|
+
# around `swarph codegraph-hook` rather than fat inline shell: the trigger
|
|
160
|
+
# heuristics are the hard-won part — each guard in that module exists because a
|
|
161
|
+
# LOOSER version misfired in production (a heredoc delimiter extracted as the
|
|
162
|
+
# search term; a quoted string lifted out of unrelated prose) — and heuristics
|
|
163
|
+
# that specific must be unit-testable, which inline sh is not. Same pattern as
|
|
164
|
+
# `install-hook`, which points at `swarph hook-output`.
|
|
165
|
+
#
|
|
166
|
+
# `exec` so the hook process IS the handler, and the verb always exits 0: a
|
|
167
|
+
# PostToolUse hook must never fail a turn.
|
|
168
|
+
_CODEGRAPH_ON_GREP_SH = r"""#!/bin/sh
|
|
169
|
+
# codegraph-on-grep.sh — swarph bundled Claude Code hook (PostToolUse/Bash).
|
|
170
|
+
#
|
|
171
|
+
# When a grep/rg searches CODE, also ask the gateway's structural codegraph and
|
|
172
|
+
# hand the symbol-level answer back as context. Supplements grep, never blocks it.
|
|
173
|
+
# All logic lives in `swarph codegraph-hook` so it can be tested; see that module
|
|
174
|
+
# for why each trigger guard exists.
|
|
175
|
+
exec swarph codegraph-hook "$@"
|
|
176
|
+
"""
|
|
177
|
+
|
|
158
178
|
BUILTIN_HOOKS: dict = {
|
|
179
|
+
"codegraph-on-grep": HookBundle(
|
|
180
|
+
name="codegraph-on-grep",
|
|
181
|
+
description=(
|
|
182
|
+
"When a grep/rg searches code, also queries the gateway's structural "
|
|
183
|
+
"codegraph and injects the symbol-level answer (definitions, file:line, "
|
|
184
|
+
"caller counts) as context. Supplements grep; never blocks it. Reports "
|
|
185
|
+
"an unavailable index LOUDLY rather than as 'no matches'."
|
|
186
|
+
),
|
|
187
|
+
publisher="swarph-builtin",
|
|
188
|
+
trust="builtin",
|
|
189
|
+
script_name="codegraph-on-grep.sh",
|
|
190
|
+
script_body=_CODEGRAPH_ON_GREP_SH,
|
|
191
|
+
bindings=(
|
|
192
|
+
HookBinding("PostToolUse", "Bash"),
|
|
193
|
+
),
|
|
194
|
+
),
|
|
159
195
|
"cell-resilience": HookBundle(
|
|
160
196
|
name="cell-resilience",
|
|
161
197
|
description=(
|
|
@@ -1045,16 +1045,93 @@ def _proc_cmdline(pid: int) -> Optional[str]:
|
|
|
1045
1045
|
return raw.replace(b"\x00", b" ").decode("utf-8", "replace").strip() or None
|
|
1046
1046
|
|
|
1047
1047
|
|
|
1048
|
-
|
|
1048
|
+
# Liveness is TRI-STATE. "I could not determine" is not "dead" — see _process_liveness.
|
|
1049
|
+
LIVENESS_ALIVE = "alive"
|
|
1050
|
+
LIVENESS_DEAD = "dead"
|
|
1051
|
+
LIVENESS_UNKNOWN = "unknown"
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
def _windows_liveness(pid: int) -> str:
|
|
1055
|
+
"""Ask Windows directly, via OpenProcess — never via os.kill.
|
|
1056
|
+
|
|
1057
|
+
PROCESS_QUERY_LIMITED_INFORMATION (0x1000) is the minimum right that answers
|
|
1058
|
+
"does this pid exist", and it is grantable across sessions where the rights
|
|
1059
|
+
os.kill needs are not. ERROR_ACCESS_DENIED means the process EXISTS and we may
|
|
1060
|
+
not inspect it — that is ALIVE, not dead. ERROR_INVALID_PARAMETER means no such
|
|
1061
|
+
pid. Anything else is UNKNOWN, never dead.
|
|
1062
|
+
"""
|
|
1063
|
+
try:
|
|
1064
|
+
import ctypes
|
|
1065
|
+
from ctypes import wintypes
|
|
1066
|
+
except Exception: # noqa: BLE001
|
|
1067
|
+
return LIVENESS_UNKNOWN
|
|
1068
|
+
ERROR_ACCESS_DENIED, ERROR_INVALID_PARAMETER, STILL_ACTIVE = 5, 87, 259
|
|
1069
|
+
try:
|
|
1070
|
+
k32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
1071
|
+
handle = k32.OpenProcess(0x1000, False, pid)
|
|
1072
|
+
if not handle:
|
|
1073
|
+
err = ctypes.get_last_error()
|
|
1074
|
+
if err == ERROR_ACCESS_DENIED:
|
|
1075
|
+
return LIVENESS_ALIVE # exists; we just cannot look
|
|
1076
|
+
if err == ERROR_INVALID_PARAMETER:
|
|
1077
|
+
return LIVENESS_DEAD # no such pid
|
|
1078
|
+
return LIVENESS_UNKNOWN
|
|
1079
|
+
try:
|
|
1080
|
+
code = wintypes.DWORD()
|
|
1081
|
+
if not k32.GetExitCodeProcess(handle, ctypes.byref(code)):
|
|
1082
|
+
return LIVENESS_UNKNOWN
|
|
1083
|
+
return LIVENESS_ALIVE if code.value == STILL_ACTIVE else LIVENESS_DEAD
|
|
1084
|
+
finally:
|
|
1085
|
+
k32.CloseHandle(handle)
|
|
1086
|
+
except Exception: # noqa: BLE001
|
|
1087
|
+
return LIVENESS_UNKNOWN
|
|
1088
|
+
|
|
1089
|
+
|
|
1090
|
+
def _process_liveness(pid: int) -> str:
|
|
1091
|
+
"""'alive' | 'dead' | 'unknown'.
|
|
1092
|
+
|
|
1093
|
+
>>> CARD #195 — `os.kill(pid, 0)` IS NOT A LIVENESS PROBE ON WINDOWS, AND ITS
|
|
1094
|
+
FAILURE WAS BEING SWALLOWED INTO "DEAD". <<< Measured by workstation-lc against
|
|
1095
|
+
a monitor that was demonstrably draining:
|
|
1096
|
+
|
|
1097
|
+
pid 6120 alive per Windows ................. True
|
|
1098
|
+
os.kill(6120, 0) from another process ...... OSError [WinError 87]
|
|
1099
|
+
-> `except OSError: return False`
|
|
1100
|
+
pid 6120 after the probe ................... STILL ALIVE
|
|
1101
|
+
|
|
1102
|
+
The error appears when probing a process the caller did NOT spawn — the worker
|
|
1103
|
+
is a grandchild of a detached cmd.exe, a different session/handle-rights context.
|
|
1104
|
+
His first throwaway test passed because he probed his own CHILD: a child-process
|
|
1105
|
+
control cannot surface this, which is why it read safe.
|
|
1106
|
+
|
|
1107
|
+
ONE LINE PRODUCED BOTH SYMPTOMS: `monitor status` reported "not running" about
|
|
1108
|
+
every healthy monitor, AND the single-instance guard believed nothing was
|
|
1109
|
+
running, so it never blocked a second start. False negative and duplicate
|
|
1110
|
+
spawning from the same swallow.
|
|
1111
|
+
|
|
1112
|
+
>>> THE RULE: "COULD NOT DETERMINE" MUST NEVER BE REPORTED AS "DEAD" BY A GUARD
|
|
1113
|
+
WHOSE JOB IS TO PREVENT A SECOND START. <<< A blocked start is visible and
|
|
1114
|
+
recoverable; a duplicate is silent and produces two writers over one cursor.
|
|
1115
|
+
So callers treat UNKNOWN as occupied, and `status` says so rather than claiming
|
|
1116
|
+
the process is running.
|
|
1117
|
+
"""
|
|
1118
|
+
if sys.platform == "win32":
|
|
1119
|
+
return _windows_liveness(pid)
|
|
1049
1120
|
try:
|
|
1050
1121
|
os.kill(pid, 0)
|
|
1051
1122
|
except ProcessLookupError:
|
|
1052
|
-
return
|
|
1123
|
+
return LIVENESS_DEAD
|
|
1053
1124
|
except PermissionError:
|
|
1054
|
-
return
|
|
1125
|
+
return LIVENESS_ALIVE # exists, owned by another user
|
|
1055
1126
|
except OSError:
|
|
1056
|
-
return
|
|
1057
|
-
return
|
|
1127
|
+
return LIVENESS_UNKNOWN # NOT dead — we simply could not tell
|
|
1128
|
+
return LIVENESS_ALIVE
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
def _process_alive(pid: int) -> bool:
|
|
1132
|
+
"""Back-compat bool. UNKNOWN counts as alive — see _process_liveness for why
|
|
1133
|
+
treating an undetermined answer as "dead" is the failure mode, not the safe one."""
|
|
1134
|
+
return _process_liveness(pid) in (LIVENESS_ALIVE, LIVENESS_UNKNOWN)
|
|
1058
1135
|
|
|
1059
1136
|
|
|
1060
1137
|
def _terminate(pid: int) -> None:
|
|
@@ -76,7 +76,12 @@ PORT = int(os.environ.get("PORT", "8788"))
|
|
|
76
76
|
# Gateway-held gbrain proxy token (POST /brain/query). Cells authenticate to
|
|
77
77
|
# THIS gateway with their mesh token; the gateway then presents ITS OWN
|
|
78
78
|
# gbrain token upstream so cells never need a separate gbrain_ credential.
|
|
79
|
-
|
|
79
|
+
# No default host. This shipped for 79 releases with a hardcoded PRIVATE TAILNET address as
|
|
80
|
+
# its fallback, so every public install pointed at one operator's box unless the deployer
|
|
81
|
+
# happened to set the variable. Unroutable from outside, so never an access hole — but it
|
|
82
|
+
# disclosed topology, and a default that silently targets someone else's host is wrong even
|
|
83
|
+
# when it cannot be reached. Unset now means UNCONFIGURED, which is the honest state.
|
|
84
|
+
GATEWAY_GBRAIN_URL = os.environ.get("GATEWAY_GBRAIN_URL", "")
|
|
80
85
|
GATEWAY_GBRAIN_TOKEN = os.environ.get("GATEWAY_GBRAIN_TOKEN", "")
|
|
81
86
|
|
|
82
87
|
# B1 Meta-Edge identity (META_EDGE_IDENTITY_CONTRACT.md). Meta-Edge SSO ISSUES
|
|
@@ -4219,6 +4224,15 @@ def _brain_query_upstream(question: str, limit: int) -> list:
|
|
|
4219
4224
|
"""Proxy a READ-ONLY gbrain query. Builds the MCP `query` call itself (never
|
|
4220
4225
|
write/admin), POSTs to GATEWAY_GBRAIN_URL with the gateway-held token, returns
|
|
4221
4226
|
the chunk array. Raises on any upstream/parse failure (caller maps to 502)."""
|
|
4227
|
+
if not GATEWAY_GBRAIN_URL:
|
|
4228
|
+
# Named, not incidental. Removing the hardcoded default turned "unconfigured"
|
|
4229
|
+
# into a reachable state for the first time, and an empty URL would otherwise
|
|
4230
|
+
# surface as a urllib ValueError — a transport error for a CONFIGURATION fault,
|
|
4231
|
+
# which sends the reader looking at the network instead of their env.
|
|
4232
|
+
raise RuntimeError(
|
|
4233
|
+
"GATEWAY_GBRAIN_URL is not set — the brain proxy is UNCONFIGURED, not "
|
|
4234
|
+
"unreachable. Set it to your gbrain MCP endpoint. (Releases up to 0.41.0 "
|
|
4235
|
+
"silently defaulted to one operator's private host; that default is gone.)")
|
|
4222
4236
|
body = {"jsonrpc": "2.0", "id": 1, "method": "tools/call",
|
|
4223
4237
|
"params": {"name": "query",
|
|
4224
4238
|
"arguments": {"query": question, "limit": limit, "expand": False}}}
|
|
@@ -84,6 +84,7 @@ _VERB_HANDLERS: dict[str, str] = {
|
|
|
84
84
|
"spawn": "swarph_cli.commands.spawn.run_spawn",
|
|
85
85
|
"install-hook": "swarph_cli.commands.install_hook.run_install_hook",
|
|
86
86
|
"hook-output": "swarph_cli.commands.hook_output.run_hook_output",
|
|
87
|
+
"codegraph-hook": "swarph_cli.commands.codegraph_hook.run_codegraph_hook",
|
|
87
88
|
"watchdog": "swarph_cli.commands.watchdog.run_watchdog",
|
|
88
89
|
"hooks": "swarph_cli.commands.hooks.run_hooks",
|
|
89
90
|
"protocol-handler": "swarph_cli.commands.protocol_handler.run_protocol_handler",
|