swarph-cli 0.72.17__tar.gz → 0.72.18__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.72.17/src/swarph_cli.egg-info → swarph_cli-0.72.18}/PKG-INFO +9 -1
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/README.md +8 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/pyproject.toml +1 -1
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/__init__.py +1 -1
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/board.py +117 -17
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/mesh.py +70 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/roster.py +196 -0
- swarph_cli-0.72.18/src/swarph_cli/quota_sync.py +252 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18/src/swarph_cli.egg-info}/PKG-INFO +9 -1
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli.egg-info/SOURCES.txt +4 -0
- swarph_cli-0.72.18/tests/test_1068_structured_accept_cli.py +104 -0
- swarph_cli-0.72.18/tests/test_1071_quota_sync.py +307 -0
- swarph_cli-0.72.18/tests/test_copilot_pane_reader.py +165 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/LICENSE +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/setup.cfg +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/__main__.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/bench/__init__.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/bench/backends.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/bench/claude_cli.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/bench/codex_cli.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/bench/pack.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/bench/prices.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/bench/providers.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/bench/quality.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/bench/refresh_prices.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/bench/rules.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/bench/runner.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/bench/signal_score.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/bench/validate.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/caller.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/capture/__init__.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/capture/harden.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/capture/lineage.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/capture/liveness.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/capture/manifest.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/capture/paths.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/capture/verify.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/cell.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/chain_token.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/channel.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/__init__.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/_content.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/_display.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/_gateway_client.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/add.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/bench.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/board_merge_check.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/brain.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/brain_ask.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/cell.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/cell_probe.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/cell_selfcheck.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/channel.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/chat.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/codegraph.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/codegraph_hook.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/codex_hook_output.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/codex_waker.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/compress.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/daemon.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/dreaming.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/event.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/gateway.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/gh_route.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/group.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/guide.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/highlight.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/hook_interpreter.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/hook_output.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/hooks.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/import_session.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/init.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/install_codex_hooks.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/install_hook.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/install_multiplexer.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/install_opencode_plugin.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/install_postcompact_hook.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/install_wake_hook.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/lane.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/mcp_server.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/memory.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/memory_emit_hook.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/memory_sync.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/monitor.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/okf_links.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/onboard.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/peer_reply_drain.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/postcompact_hook_output.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/protocol_handler.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/ratify.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/rowclear_hook.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/schedule.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/security.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/service.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/spawn.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/sweep.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/timeline.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/triage.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/version.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/wake_hook_output.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/commands/watchdog.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/compress/__init__.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/compress/levers.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/compress/marker.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/compress/verify.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/console_safe.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/delivery_queue.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/dm_frame.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/dreaming/__init__.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/dreaming/candidates.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/dreaming/clone.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/dreaming/enrich.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/dreaming/index_check.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/dreaming/notify.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/dreaming/organize.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/dreaming/probes.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/dreaming/report.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/dreaming/run.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/dreaming/slm.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/dreaming/transcripts.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/dreaming/verify.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/gateway/__init__.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/gateway/feature_registry.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/gateway/lanes_control.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/gateway/schema.sql +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/gateway/server.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/gateway/services_control.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/gateway_default.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/gh_identity.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/guide/GUIDE.md +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/identity.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/main.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/multiplexer.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/operator/__init__.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/operator/__main__.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/operator/api.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/operator/cli.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/operator/config.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/operator/fmt.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/orphan_daemons.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/pane_probe.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/parsers/__init__.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/parsers/claude.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/payloads/opencode/plugin.js +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/payloads/postcompact/_shim.cmd +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/payloads/postcompact/postcompact-recall-grok.sh +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/payloads/postcompact/postcompact-recall.sh +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/payloads/postcompact/posttooluse-emit-antigravity.sh +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/payloads/postcompact/posttooluse-emit-claude.sh +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/payloads/postcompact/posttooluse-emit-grok.sh +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/payloads/postcompact/posttooluse-emit.sh +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/payloads/postcompact/posttooluse-recall.sh +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/payloads/postcompact/precompact-flag.sh +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/payloads/postcompact/preinvocation-recall-antigravity.sh +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/payloads/postcompact/sessionstart-recall.sh +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/peer_executor.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/peer_executor_windows.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/peer_reconciliation.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/peer_reply_outbox.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/peer_service_host.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/peer_service_payload.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/peer_service_reply.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/peer_staging.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/query_filters.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/rowclear.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/scripts/__init__.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/scripts/dm_notify_filter.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/scripts/ensure_monitor.sh +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/scripts/install_codex_waker_windows.ps1 +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/scripts/install_monitor_task_windows.ps1 +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/scripts/wake_watchdog.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/scrub.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/service/__init__.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/service/app.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/service/providers.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/session_bridge.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/skills/i-have-adhd/LICENSE +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/skills/i-have-adhd/SKILL.md +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/source_text.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/stall_alert.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/systemd/codex-waker.default +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/systemd/peer-reply-drain.default +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/systemd/swarph-codex-outbox@.service +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/systemd/swarph-codex-outbox@.timer +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/systemd/swarph-codex-waker@.service +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/systemd/swarph-codex-waker@.timer +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/systemd/swarph-monitor-reexec.path +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/systemd/swarph-monitor-reexec.service +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/systemd/swarph-monitor@.service +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/systemd/swarph-peer-reply-drain@.service +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/systemd/swarph-peer-reply-drain@.timer +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/systemd/swarph-stuck-sweep.service +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/systemd/swarph-stuck-sweep.timer +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/systemd/swarph-triage.service +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/systemd/swarph-triage.timer +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/timeline_paths.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/tokens.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/triage/__init__.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/triage/backend.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/triage/job.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/triage/owners.json +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/triage/router.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/triage/rows.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli/triage/sensor.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli.egg-info/entry_points.txt +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli.egg-info/requires.txt +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/src/swarph_cli.egg-info/top_level.txt +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_1006_board_publisher.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_1006_session_board.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_1010_strand_only.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_1013_spawn_channel.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_1017_muse_tui.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_1050_surrogate_wake.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_1052_opencode_deaf.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_1059_watchdog_unread_env.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_1061_roster_cli.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_1064_stuck_sweep.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_1065_release.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_1066_quiet_wakes.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_1073_triage.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_1073_triage_board.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_1073_wheel_install.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_1075_task_cli.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_1083_openai_decisions.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_124_register_merge.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_126_pending_from_complete.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_132_ensure_monitor_state_dir.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_138_memory_navigate.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_139_tool_probe.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_183_card_anchor.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_184_notify_honesty.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_201_win32_bash_refusal.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_243_onboard_per_peer_token.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_307_task4_ask_reply.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_312_codegraph_fuzzy_verb.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_319_refusal_exit_codes.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_323_mistral_backend.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_332_report_organize_findings.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_356_query_filters.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_397_gh_identity_router.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_397_router_preflight.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_402_one_identity_resolver.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_432_rowclear.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_467b_onboard_operator_credential.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_472_register_caller_binding.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_484_codex_queue_sink.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_523_guide.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_524_conftest_hermeticity.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_532_accept_flag.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_535_version_capability.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_538_signal_bench.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_546_default_gateway.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_547_verbs_from_registry.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_548_gateway_remainder.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_552_codegraph_gateway.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_555_relay_row_shape.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_556_relay_freshness.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_559_edges_truncated.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_564_bootstrap_first_peer.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_578_no_machine_specific_host_defaults.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_579_register_bootstrap_message.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_591_cli_step_graph.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_644_monitor_supervision.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_648_install_unit.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_654_reexec.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_656_owned_slm.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_684_dreaming_verb.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_689_pkg_version_order.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_715_grok_wake.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_716_timeline_one_path.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_723_grok_0251.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_724_ps1_ascii_or_bom.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_725_stdio_reconfigure.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_726_grok_limit_screen.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_729_arm_check_delivery_gap.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_729_dm_once.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_729_wake_watchdog.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_734_dreaming_enrich_degrade.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_735_dm_mcp_tools.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_739_wake_writer.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_740_edit_project.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_762_cards_edit_priority.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_764_enrich_collapse.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_801_confirm_flow_cli.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_802_stage_history.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_825_codegraph_hook_noise.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_825_install_posttooluse_only.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_825_remaining_cap_shred_bind.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_829_counterfactual_column.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_830_hook_binding_version.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_84_cli_labels.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_866_doctrine_guide.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_872_identity_refuses_not_warns.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_874_source_text.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_888_opencode_data_dir_can_fail.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_937_dreaming_notify.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_938_organize_line_limit.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_941_bench_backends.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_941_harness_342.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_945_same_line_pairing.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_955_prompt_audit.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_957_auth_http.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_959_venv_interpreter.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_960_channel.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_960_ensure_monitor_heartbeat.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_961_cursor_grok_footer.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_961_loud_stop.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_963_plugin_manifest.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_964_channel_cell.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_965_mesh_wait.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_975_channel_hop.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_add_security_gate.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_artifact_add.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_artifact_hash.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_artifact_lib.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_artifact_mcp.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_artifact_skill.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_artifact_tool.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_artifact_uri.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_bench_backends.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_bench_claude_cli.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_bench_codex_cli.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_bench_command.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_bench_pack.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_bench_prices.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_bench_quality.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_bench_runner.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_bench_validate.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_board_card_thread.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_board_command.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_board_merge_check.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_board_obligation_close.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_brain_ask_command.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_brain_ask_env_diagnosis.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_brain_ask_identity_explicitness.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_brain_command.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_caller_meta_guard.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_capture_lineage.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_capture_liveness.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_capture_manifest.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_capture_paths.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_capture_security.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_cell_command_dispatch.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_cell_harden.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_cell_loader.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_cell_selfcheck.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_cell_verify.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_chain_token.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_channel_command.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_channel_poll_never_breaks_dms.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_channel_post.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_channel_wake_policy_filter.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_chat_command.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_claude_parser.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_claude_tmux_template.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_codegraph_caller_scope.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_codegraph_command.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_codegraph_hook.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_codegraph_mcp.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_codex_waker.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_compress_caller_convention.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_compress_command.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_compress_levers.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_compress_marker.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_compress_verify.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_console_encoding_e2e.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_content_composition.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_copilot_membrane.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_cursor_membrane.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_daemon_command.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_daemon_console_encoding.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_daemon_delivery.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_delivery_queue.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_display_sanitize.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_ensure_monitor_no_peer_default.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_event_emit.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_feature_to_uri.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_gateway_brain.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_gateway_client.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_gateway_command.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_gateway_message_idempotency.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_gh_route_doctor_mesh_name.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_group_command.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_guide_commands_parse.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_guide_selfcheck.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_heartbeat_check.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_highlight_command.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_hook_output.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_hooks_activity_marker.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_hooks_add.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_hooks_bundle.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_hooks_init.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_hooks_legacy_generation_removal.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_hooks_lifecycle.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_hooks_merge.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_hooks_verify.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_hooks_win32_bash_wrapper.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_hooks_win32_command_path.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_import_command.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_inbox_identity_provenance.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_init_command.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_init_provider_menu.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_install_codex_hooks.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_install_hook.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_install_multiplexer_command.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_install_postcompact_hook.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_install_wake_hook.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_lane_command.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_main.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_main_unknown_verb_guard.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_mcp_missing_hint.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_mcp_server.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_memory_command.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_memory_emit_snippet.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_memory_gateway.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_memory_sync.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_memory_sync_membrane.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_mesh_command.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_mesh_inbox_mark_read.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_mesh_inbox_sanitize.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_mesh_originate_thread_623.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_mesh_send_unknown_peer.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_mesh_sidecar.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_mesh_sidecar_cursor_decoupled.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_mesh_sidecar_muse_sink.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_mesh_sidecar_wake_survives_read.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_meta_edge_identity.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_monitor_channel_poll.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_monitor_cli.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_monitor_daemon_collision.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_monitor_self_name_verification.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_monitor_state_split.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_multiplexer.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_muse_membrane.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_muse_own_cli.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_obligation_close_is_surfaced.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_okf_links.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_onboard_command.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_opencode_membrane.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_opencode_plugin.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_operator_me.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_orphan_daemons.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_packaged_artifacts_exist.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_packaged_skills.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_peer_executor.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_peer_executor_windows.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_peer_reply_drain_command.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_peer_reply_drain_supervisor.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_peer_reply_outbox.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_peer_reply_transport.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_peer_service_e2e.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_peer_service_host.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_peer_service_payload.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_peer_service_reply.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_postcompact_recall.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_process_liveness.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_protocol_handler.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_providers_isolation.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_ratify_command.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_readme_claims_no_gateway_default.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_repl_caller_convention.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_schedule_command.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_schedule_create_contract.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_scrub.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_security.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_service_app.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_service_command.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_service_providers.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_session_bridge_inject.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_session_bridge_probe.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_session_bridge_resolve.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_session_path_role_gate.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_shipped_units_match_the_cli.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_smoke_chat.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_smoke_one_shot.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_smoke_phase_5_5.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_spawn_base_hoist.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_spawn_command.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_spawn_identity_stamp.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_spawn_live_pin.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_spawn_mitosis_lineage.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_spawn_tmux_session.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_spawn_windows_relaunch.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_stall_alert.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_subprocess_decode_encoding.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_timeline_command.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_tmux_wake_runstate_defer.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_tmux_wake_session_reanchor.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_tmux_wake_stale.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_tmux_wake_submit_verify.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_token_file_one_parser.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_token_precedence_explicit_first.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_unified_token_resolver.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_version_command.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_version_consistency.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_vibe_membrane.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_watchdog.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_watchdog_dm_wake.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_watchdog_dm_wake_wiring.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_watchdog_emit_health.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_watchdog_install_flags.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_watchdog_model_rung.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_watchdog_stale_peers.py +0 -0
- {swarph_cli-0.72.17 → swarph_cli-0.72.18}/tests/test_windows_waker_installer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: swarph-cli
|
|
3
|
-
Version: 0.72.
|
|
3
|
+
Version: 0.72.18
|
|
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`, per-invocation `gh` identity routing, a silent DM-wake hook, session `import`, and a stranded-session `watchdog`.
|
|
5
5
|
Author: Pierre Samson, Claude Opus
|
|
6
6
|
License: MIT
|
|
@@ -497,6 +497,14 @@ swarph monitor stop
|
|
|
497
497
|
| `none` | genuinely nothing: observe, append `inbox.log`, no ledger, **no unread tracking**. |
|
|
498
498
|
| `webhook:<url>` | **held** pending an egress review — exits non-zero rather than silently no-op. |
|
|
499
499
|
|
|
500
|
+
**Copilot tmux panes.** The reader recognizes Copilot CLI 1.0.94's color input
|
|
501
|
+
box and `--no-color` composer using the bottom AIC summary, input borders, and
|
|
502
|
+
status footer. It defers while `esc interrupt` indicates a running turn or
|
|
503
|
+
the composer holds a human draft. The wake names `swarph_dm_unread`; delivery
|
|
504
|
+
is recorded only after the composer is observed clear. Missing or incomplete
|
|
505
|
+
pane captures send no keystrokes and leave delivery pending with a visible
|
|
506
|
+
failure. Keep `pull` for layouts that hide or customize this chrome.
|
|
507
|
+
|
|
500
508
|
**Two pieces of state that never share a variable.** The *observation cursor* (`cursor.json`) is what this monitor has READ from the gateway; it advances on observation, always, gated on nothing. A *delivery ledger* (`ledgers.json`, one per sink) advances only when that sink is satisfied and may lag arbitrarily far. So a dead tmux pane can no longer freeze the cursor, one dead sink cannot stall another, and a sink attached tomorrow starts with an empty ledger and replays from `inbox.log` (bounded, and it reports what it skipped).
|
|
501
509
|
|
|
502
510
|
**Pull beats push.** Every push sink's liveness is a precondition for hearing anything — if the pane dies, "no wake arrived" is indistinguishable from "no mail arrived". A pull check run *by* the cell lives one layer above tmux and cannot die with it. Designed to drop straight into a SessionStart hook:
|
|
@@ -448,6 +448,14 @@ swarph monitor stop
|
|
|
448
448
|
| `none` | genuinely nothing: observe, append `inbox.log`, no ledger, **no unread tracking**. |
|
|
449
449
|
| `webhook:<url>` | **held** pending an egress review — exits non-zero rather than silently no-op. |
|
|
450
450
|
|
|
451
|
+
**Copilot tmux panes.** The reader recognizes Copilot CLI 1.0.94's color input
|
|
452
|
+
box and `--no-color` composer using the bottom AIC summary, input borders, and
|
|
453
|
+
status footer. It defers while `esc interrupt` indicates a running turn or
|
|
454
|
+
the composer holds a human draft. The wake names `swarph_dm_unread`; delivery
|
|
455
|
+
is recorded only after the composer is observed clear. Missing or incomplete
|
|
456
|
+
pane captures send no keystrokes and leave delivery pending with a visible
|
|
457
|
+
failure. Keep `pull` for layouts that hide or customize this chrome.
|
|
458
|
+
|
|
451
459
|
**Two pieces of state that never share a variable.** The *observation cursor* (`cursor.json`) is what this monitor has READ from the gateway; it advances on observation, always, gated on nothing. A *delivery ledger* (`ledgers.json`, one per sink) advances only when that sink is satisfied and may lag arbitrarily far. So a dead tmux pane can no longer freeze the cursor, one dead sink cannot stall another, and a sink attached tomorrow starts with an empty ledger and replays from `inbox.log` (bounded, and it reports what it skipped).
|
|
452
460
|
|
|
453
461
|
**Pull beats push.** Every push sink's liveness is a precondition for hearing anything — if the pane dies, "no wake arrived" is indistinguishable from "no mail arrived". A pull check run *by* the cell lives one layer above tmux and cannot die with it. Designed to drop straight into a SessionStart hook:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "swarph-cli"
|
|
7
|
-
version = "0.72.
|
|
7
|
+
version = "0.72.18"
|
|
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`, per-invocation `gh` identity routing, a silent DM-wake hook, session `import`, and a stranded-session `watchdog`."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -132,6 +132,12 @@ def _format_obligations(data: dict) -> str:
|
|
|
132
132
|
marks.append(f"UNCLOSABLE:{_s(o['unclosable_reason'])}")
|
|
133
133
|
state = _s(o.get("accept_state"))
|
|
134
134
|
marks.append(f"accept:{state}" if state else "accept:missing")
|
|
135
|
+
if o.get("accept_format") == "json":
|
|
136
|
+
# #1068: a structured row reads evidenced/total — the reviewer
|
|
137
|
+
# checks condition by condition (--json carries each pair).
|
|
138
|
+
conds = o.get("conditions") or []
|
|
139
|
+
ev = sum(1 for x in conds if x.get("evidence"))
|
|
140
|
+
marks.append(f"conds:{ev}/{len(conds)}")
|
|
135
141
|
if o.get("status") != "open":
|
|
136
142
|
# a NULL outcome on a closed row is a pre-#562 legacy close — name
|
|
137
143
|
# the absence rather than print a blank that reads as recorded
|
|
@@ -547,6 +553,28 @@ def _card_say_payload(from_node: str, to_node: str, kind: str, content: str,
|
|
|
547
553
|
_NO_TIMEOUT = "NO TIMEOUT — this never goes red on its own; pass --timeout-hours if it should"
|
|
548
554
|
|
|
549
555
|
|
|
556
|
+
def _parse_accept_conditions(accept):
|
|
557
|
+
"""#1068: read a stored accept the way the gateway does — a JSON row
|
|
558
|
+
yields {"pass", "fail"}, prose or missing yields None. Never raises:
|
|
559
|
+
a render helper must not 500 a listing over one odd row."""
|
|
560
|
+
if not accept or not str(accept).strip():
|
|
561
|
+
return None
|
|
562
|
+
try:
|
|
563
|
+
obj = json.loads(str(accept))
|
|
564
|
+
except (ValueError, TypeError):
|
|
565
|
+
return None
|
|
566
|
+
if not isinstance(obj, dict) or not isinstance(obj.get("pass"), list):
|
|
567
|
+
return None
|
|
568
|
+
passes = [p for p in obj["pass"]
|
|
569
|
+
if isinstance(p, dict) and isinstance(p.get("id"), str)
|
|
570
|
+
and isinstance(p.get("text"), str)]
|
|
571
|
+
if len(passes) != len(obj["pass"]) or not passes:
|
|
572
|
+
return None
|
|
573
|
+
fails = obj.get("fail", [])
|
|
574
|
+
return {"pass": [{"id": p["id"], "text": p["text"]} for p in passes],
|
|
575
|
+
"fail": [f for f in fails if isinstance(f, str)]}
|
|
576
|
+
|
|
577
|
+
|
|
550
578
|
def _accept_line(accept) -> str:
|
|
551
579
|
"""#532's mint-time falsifier readout — shared by the pre-#591 and the §2
|
|
552
580
|
success lines, because the rule is about the MOMENT, not the response
|
|
@@ -556,6 +584,13 @@ def _accept_line(accept) -> str:
|
|
|
556
584
|
return ("NO ACCEPT CHECK — reads RED in the sweep; pass --accept "
|
|
557
585
|
"\"PASS = ... | FAIL = ...\" naming an observable and a way "
|
|
558
586
|
"it comes out negative")
|
|
587
|
+
parsed = _parse_accept_conditions(accept)
|
|
588
|
+
if parsed is not None:
|
|
589
|
+
# #1068: a structured check reads condition by condition, not as a blob.
|
|
590
|
+
line = "; ".join(f"{p['id']}: {p['text']}" for p in parsed["pass"])
|
|
591
|
+
if parsed["fail"]:
|
|
592
|
+
line += " | FAIL: " + "; ".join(parsed["fail"])
|
|
593
|
+
return f"accept: {line}"
|
|
559
594
|
if not _FAIL_MARKER_RE.search(str(accept)):
|
|
560
595
|
return ("accept check has NO FAIL BRANCH — marked NO-FAIL-BRANCH in "
|
|
561
596
|
"the sweep; a check that cannot come out negative is not one")
|
|
@@ -630,15 +665,28 @@ def _step_list(values) -> list[str]:
|
|
|
630
665
|
return [n for n in names if n]
|
|
631
666
|
|
|
632
667
|
|
|
668
|
+
def _json_flag(flag: str, value: str):
|
|
669
|
+
"""Parse a --*-json flag locally (#1068): invalid JSON is refused here
|
|
670
|
+
(exit 2 via the caller's ValueError catch), never sent as a string the
|
|
671
|
+
gateway would have to guess at."""
|
|
672
|
+
try:
|
|
673
|
+
return json.loads(value)
|
|
674
|
+
except (ValueError, TypeError) as exc:
|
|
675
|
+
raise ValueError(f"{flag} is not valid JSON: {exc}")
|
|
676
|
+
|
|
677
|
+
|
|
633
678
|
def _ask_payload(self_name, what, *, holder=None, step=None, needs=None, hours=None,
|
|
634
|
-
done=None, accept=None, kind="action",
|
|
635
|
-
task=None, task_needs=None) -> dict:
|
|
679
|
+
done=None, accept=None, accept_json=None, kind="action",
|
|
680
|
+
timeout_hours=None, task=None, task_needs=None) -> dict:
|
|
636
681
|
"""POST /board/cards/{id}/ask body. None = not mentioned = key ABSENT (#191's
|
|
637
682
|
rule; test_532 pins that a null `accept` on the wire would overwrite the
|
|
638
683
|
gateway's own normalization). No holder → `requested`; `me` → the caller's
|
|
639
684
|
own `--as` identity (the contract binds `me` to the token, never to an env
|
|
640
685
|
var). `needs` is whatever `--needs` collected — see `_step_list`.
|
|
641
686
|
"""
|
|
687
|
+
if accept is not None and accept_json is not None:
|
|
688
|
+
raise ValueError("pass --accept or --accept-json, not both — "
|
|
689
|
+
"one check per row")
|
|
642
690
|
p = {"what": what, "created_by": self_name, "kind": kind}
|
|
643
691
|
if holder is not None:
|
|
644
692
|
p["holder"] = self_name if holder == "me" else holder
|
|
@@ -652,6 +700,8 @@ def _ask_payload(self_name, what, *, holder=None, step=None, needs=None, hours=N
|
|
|
652
700
|
p["done"] = done
|
|
653
701
|
if accept is not None:
|
|
654
702
|
p["accept"] = accept
|
|
703
|
+
if accept_json is not None:
|
|
704
|
+
p["accept_json"] = accept_json
|
|
655
705
|
if timeout_hours is not None:
|
|
656
706
|
p["timeout_hours"] = timeout_hours
|
|
657
707
|
if task is not None:
|
|
@@ -818,8 +868,25 @@ def _move_line(x) -> str:
|
|
|
818
868
|
return line
|
|
819
869
|
|
|
820
870
|
|
|
871
|
+
def _close_payload(outcome, evidence=None, evidence_json=None) -> dict:
|
|
872
|
+
"""POST /board/obligations/{id}/close body (#1068): --evidence and/or
|
|
873
|
+
--evidence-json, at least one. The CLI cannot know the row's stored
|
|
874
|
+
accept, so it sends what it was given — the gateway requires the map
|
|
875
|
+
for a JSON pass, the string everywhere else."""
|
|
876
|
+
if evidence is None and evidence_json is None:
|
|
877
|
+
raise ValueError("nothing observed — pass --evidence and/or "
|
|
878
|
+
"--evidence-json")
|
|
879
|
+
p = {"outcome": outcome}
|
|
880
|
+
if evidence is not None:
|
|
881
|
+
p["evidence"] = evidence
|
|
882
|
+
if evidence_json is not None:
|
|
883
|
+
p["evidence_json"] = evidence_json
|
|
884
|
+
return p
|
|
885
|
+
|
|
886
|
+
|
|
821
887
|
def _amend_payload(step=None, holder=None, accept=None, hours=None,
|
|
822
|
-
needs_add=None, needs_remove=None, needs=None
|
|
888
|
+
needs_add=None, needs_remove=None, needs=None,
|
|
889
|
+
accept_json=None) -> dict:
|
|
823
890
|
"""PATCH /board/obligations/{id}/amend body. None = not mentioned; an empty
|
|
824
891
|
`holder` is a REAL value (clears → `requested`), so the test is
|
|
825
892
|
`is not None`, never truthiness (#191). `needs` is the contract's signed
|
|
@@ -827,15 +894,19 @@ def _amend_payload(step=None, holder=None, accept=None, hours=None,
|
|
|
827
894
|
needs_remove; every edge list flattens through `_step_list`. Refuses the
|
|
828
895
|
empty patch locally — the gateway 400s it too, but a round trip to learn
|
|
829
896
|
you said nothing is the #256 shape one hop later."""
|
|
897
|
+
if accept is not None and accept_json is not None:
|
|
898
|
+
raise ValueError("pass --accept or --accept-json, not both — "
|
|
899
|
+
"one check per row")
|
|
830
900
|
signed = _step_list(needs)
|
|
831
901
|
add = _step_list(needs_add) + [x for x in signed if not x.startswith("-")]
|
|
832
902
|
rem = _step_list(needs_remove) + [x[1:] for x in signed if x.startswith("-")]
|
|
833
903
|
patch = {k: v for k, v in (("step", step), ("holder", holder), ("accept", accept),
|
|
904
|
+
("accept_json", accept_json),
|
|
834
905
|
("hours", hours), ("needs_add", add or None),
|
|
835
906
|
("needs_remove", rem or None)) if v is not None}
|
|
836
907
|
if not patch:
|
|
837
|
-
raise ValueError("nothing to amend — pass --step, --holder, --accept, --
|
|
838
|
-
"--needs-add and/or --needs-remove")
|
|
908
|
+
raise ValueError("nothing to amend — pass --step, --holder, --accept, --accept-json, "
|
|
909
|
+
"--hours, --needs-add and/or --needs-remove")
|
|
839
910
|
return patch
|
|
840
911
|
|
|
841
912
|
|
|
@@ -968,6 +1039,11 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
968
1039
|
"(#532). Omitting it mints an obligation that reads RED "
|
|
969
1040
|
"in the sweep; a check with no FAIL branch is marked "
|
|
970
1041
|
"NO-FAIL-BRANCH — 'verify it works' is not a check")
|
|
1042
|
+
ck.add_argument("--accept-json", default=None, metavar="'{...}'",
|
|
1043
|
+
help="the structured check (#1068): '{\"pass\": [{\"id\", "
|
|
1044
|
+
"\"text\"}], \"fail\": [...]}'. Mutually exclusive "
|
|
1045
|
+
"with --accept; a PASS close of such a row needs "
|
|
1046
|
+
"--evidence-json keyed by condition id")
|
|
971
1047
|
ck.add_argument("--kind", default="action", help="obligation kind (default: action)")
|
|
972
1048
|
# #591: the step graph. `--holder` is an OPTION with its own dest — sharing
|
|
973
1049
|
# the positional's dest makes `--holder me --step build "<what>"` parse
|
|
@@ -1063,10 +1139,15 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
1063
1139
|
choices=("pass", "fail", "cannot_evaluate", "skipped"),
|
|
1064
1140
|
help="the accept check's OUTCOME (`skipped` only on an "
|
|
1065
1141
|
"optional step — #591)")
|
|
1066
|
-
oc.add_argument("--evidence", required=
|
|
1142
|
+
oc.add_argument("--evidence", required=False, default=None,
|
|
1067
1143
|
help="what you OBSERVED running the check — whitespace is "
|
|
1068
1144
|
"refused: whitespace evidence is the vibe-close this "
|
|
1069
1145
|
"endpoint exists to kill")
|
|
1146
|
+
oc.add_argument("--evidence-json", default=None, metavar="'{...}'",
|
|
1147
|
+
help="the per-condition evidence map (#1068): '{\"c1\": "
|
|
1148
|
+
"\"...\", ...}' keyed by the row's condition ids. "
|
|
1149
|
+
"Pass --evidence and/or --evidence-json; the gateway "
|
|
1150
|
+
"requires what the row's stored accept demands")
|
|
1070
1151
|
oc.add_argument("--json", action="store_true"); _add_common(oc)
|
|
1071
1152
|
|
|
1072
1153
|
ol = obl.add_parser(
|
|
@@ -1101,6 +1182,9 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
1101
1182
|
oa.add_argument("--holder", default=None, metavar="PEER",
|
|
1102
1183
|
help="reassign; pass an empty string to clear (back to requested)")
|
|
1103
1184
|
oa.add_argument("--accept", default=None, metavar='"PASS = ... | FAIL = ..."')
|
|
1185
|
+
oa.add_argument("--accept-json", default=None, metavar="'{...}'",
|
|
1186
|
+
help="replace the check with a structured one (#1068); "
|
|
1187
|
+
"mutually exclusive with --accept")
|
|
1104
1188
|
oa.add_argument("--hours", type=int, default=None, help="clock override, up to 4x the menu value")
|
|
1105
1189
|
oa.add_argument("--needs-add", action="append", dest="needs_add", default=None,
|
|
1106
1190
|
metavar="STEP", help="add an edge (repeatable)")
|
|
@@ -1172,17 +1256,23 @@ def run_board(argv: list[str]) -> int:
|
|
|
1172
1256
|
# #562's CLI half: the explicit close act. The gateway guards the
|
|
1173
1257
|
# identity (holder, creator, or board orchestrator) and refuses a
|
|
1174
1258
|
# second close; the CLI's job is to refuse what it can see —
|
|
1175
|
-
# whitespace evidence
|
|
1176
|
-
# with their detail intact.
|
|
1259
|
+
# whitespace evidence, invalid JSON, neither evidence — and to
|
|
1260
|
+
# propagate the gateway's refusals with their detail intact.
|
|
1177
1261
|
evidence = args.evidence
|
|
1178
|
-
if not evidence.strip():
|
|
1262
|
+
if evidence is not None and not evidence.strip():
|
|
1179
1263
|
print("swarph board obligations close: --evidence is "
|
|
1180
1264
|
"whitespace-only — the close act records what you "
|
|
1181
1265
|
"OBSERVED, and nothing was observed", file=sys.stderr)
|
|
1182
1266
|
return 2
|
|
1267
|
+
try:
|
|
1268
|
+
evidence_json = (_json_flag("--evidence-json", args.evidence_json)
|
|
1269
|
+
if args.evidence_json is not None else None)
|
|
1270
|
+
payload = _close_payload(args.outcome, evidence, evidence_json)
|
|
1271
|
+
except ValueError as exc:
|
|
1272
|
+
print(f"swarph board obligations close: {exc}", file=sys.stderr)
|
|
1273
|
+
return 2
|
|
1183
1274
|
st, d = _post_json(
|
|
1184
|
-
f"{gw}/board/obligations/{args.id}/close",
|
|
1185
|
-
{"outcome": args.outcome, "evidence": evidence}, token)
|
|
1275
|
+
f"{gw}/board/obligations/{args.id}/close", payload, token)
|
|
1186
1276
|
if st < 200 or st >= 300:
|
|
1187
1277
|
detail = d.get("detail", d) if isinstance(d, dict) else d
|
|
1188
1278
|
print(f"swarph board obligations close: gateway {st}: {_s(detail)}",
|
|
@@ -1212,10 +1302,13 @@ def run_board(argv: list[str]) -> int:
|
|
|
1212
1302
|
return _out(st, d, _obligation_act_line, aj)
|
|
1213
1303
|
if args.command == "amend":
|
|
1214
1304
|
try:
|
|
1305
|
+
accept_json = (_json_flag("--accept-json", args.accept_json)
|
|
1306
|
+
if args.accept_json is not None else None)
|
|
1215
1307
|
patch = _amend_payload(step=args.step, holder=args.holder,
|
|
1216
1308
|
accept=args.accept, hours=args.hours,
|
|
1217
1309
|
needs_add=args.needs_add,
|
|
1218
|
-
needs_remove=args.needs_remove, needs=args.needs
|
|
1310
|
+
needs_remove=args.needs_remove, needs=args.needs,
|
|
1311
|
+
accept_json=accept_json)
|
|
1219
1312
|
except ValueError as exc:
|
|
1220
1313
|
print(f"swarph board obligations amend: {exc}", file=sys.stderr)
|
|
1221
1314
|
return 2
|
|
@@ -1388,11 +1481,18 @@ def run_board(argv: list[str]) -> int:
|
|
|
1388
1481
|
f"or positional). If {args.what!r} was meant as the holder, the "
|
|
1389
1482
|
f"\"<what>\" text is missing", file=sys.stderr)
|
|
1390
1483
|
return 2
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1484
|
+
try:
|
|
1485
|
+
accept_json = (_json_flag("--accept-json", args.accept_json)
|
|
1486
|
+
if args.accept_json is not None else None)
|
|
1487
|
+
body = _ask_payload(self_name, args.what, holder=holder,
|
|
1488
|
+
step=args.step, needs=args.needs, hours=args.hours,
|
|
1489
|
+
done=args.done, accept=args.accept,
|
|
1490
|
+
accept_json=accept_json, kind=args.kind,
|
|
1491
|
+
timeout_hours=args.timeout_hours,
|
|
1492
|
+
task=args.task, task_needs=args.task_needs)
|
|
1493
|
+
except ValueError as exc:
|
|
1494
|
+
print(f"swarph board cards ask: {exc}", file=sys.stderr)
|
|
1495
|
+
return 2
|
|
1396
1496
|
st, d = _post_json(f"{gw}/board/cards/{args.id}/ask", body, token)
|
|
1397
1497
|
return _out(st, d, _ask_line, args.json)
|
|
1398
1498
|
if args.command == "confirm-flow":
|
|
@@ -2350,6 +2350,7 @@ _GROK_WAKE_PROMPT = "check mesh: swarph_dm_unread"
|
|
|
2350
2350
|
# Muse 1.4.1 has no push channel either. The wake names the pull tool,
|
|
2351
2351
|
# same text the grok and opencode sinks inject.
|
|
2352
2352
|
_MUSE_WAKE_PROMPT = "check mesh: swarph_dm_unread"
|
|
2353
|
+
_COPILOT_WAKE_PROMPT = "check mesh: swarph_dm_unread"
|
|
2353
2354
|
# A finished turn keeps its ▣ header and adds a duration.
|
|
2354
2355
|
# Short turns read `· 3.3s` (drop-on-meta-edge, 1.18.33, card #961 post
|
|
2355
2356
|
# 54557). A longer idle turn reads `· 3m 4s` (opencode pane, 2026-09-29
|
|
@@ -2991,6 +2992,8 @@ def _muse_running(lines: list[str]) -> bool:
|
|
|
2991
2992
|
|
|
2992
2993
|
|
|
2993
2994
|
def _wake_prompt_for(lines: Optional[list[str]]) -> str:
|
|
2995
|
+
if lines and _copilot_frame(lines) is not None:
|
|
2996
|
+
return _COPILOT_WAKE_PROMPT
|
|
2994
2997
|
if lines and _is_grok_pane(lines):
|
|
2995
2998
|
return _GROK_WAKE_PROMPT
|
|
2996
2999
|
if lines and _is_opencode_pane(lines):
|
|
@@ -3013,6 +3016,67 @@ def _sink_failure_detail(target: str) -> str:
|
|
|
3013
3016
|
return "the sink is probably gone (session restart / renamed target)"
|
|
3014
3017
|
|
|
3015
3018
|
|
|
3019
|
+
def _copilot_frame(
|
|
3020
|
+
lines: list[str],
|
|
3021
|
+
) -> Optional[tuple[Optional[str], Optional[bool]]]:
|
|
3022
|
+
"""Read Copilot's bottom input block, not transcript quotes (#386).
|
|
3023
|
+
|
|
3024
|
+
Both measured 1.0.94 layouts put the AIC summary immediately above
|
|
3025
|
+
the input: ``--no-color`` uses rules and ``❯``; color uses a ``┃``
|
|
3026
|
+
box. An identified but incomplete block stays unknown.
|
|
3027
|
+
"""
|
|
3028
|
+
tail = _nonempty_tail(lines, 12)
|
|
3029
|
+
summary = next(
|
|
3030
|
+
(i for i in range(len(tail) - 1, -1, -1)
|
|
3031
|
+
if re.search(r"Plan: .* · Session: .* AIC\b", tail[i])),
|
|
3032
|
+
None,
|
|
3033
|
+
)
|
|
3034
|
+
if summary is None:
|
|
3035
|
+
return None
|
|
3036
|
+
block = [ln.strip() for ln in tail[summary + 1:]]
|
|
3037
|
+
if not block:
|
|
3038
|
+
return (None, None)
|
|
3039
|
+
text = None
|
|
3040
|
+
running = None
|
|
3041
|
+
if block[0].startswith("╻▄"):
|
|
3042
|
+
bottom = next(
|
|
3043
|
+
(i for i, ln in enumerate(block) if ln.startswith("╹▀")), None,
|
|
3044
|
+
)
|
|
3045
|
+
if bottom is not None and bottom + 1 < len(block) - 1:
|
|
3046
|
+
return None # This block is history above another composer.
|
|
3047
|
+
if bottom is not None and bottom + 1 == len(block) - 1:
|
|
3048
|
+
footer = block[-1]
|
|
3049
|
+
rows = block[1:bottom]
|
|
3050
|
+
if rows and all(ln.startswith("┃") for ln in rows):
|
|
3051
|
+
text = "\n".join(ln[1:].strip() for ln in rows).strip()
|
|
3052
|
+
if "esc interrupt" in footer:
|
|
3053
|
+
running = True
|
|
3054
|
+
elif "Interactive ·" in footer or "· autopilot ·" in footer:
|
|
3055
|
+
running = False
|
|
3056
|
+
elif _is_muse_rule(block[0]):
|
|
3057
|
+
bottom = next(
|
|
3058
|
+
(i for i in range(1, len(block))
|
|
3059
|
+
if _is_muse_rule(block[i])), None,
|
|
3060
|
+
)
|
|
3061
|
+
if bottom is not None and bottom + 1 < len(block) - 1:
|
|
3062
|
+
return None
|
|
3063
|
+
if bottom is not None and bottom + 1 == len(block) - 1:
|
|
3064
|
+
rows = block[1:bottom]
|
|
3065
|
+
if rows and rows[0].startswith("❯"):
|
|
3066
|
+
text = "\n".join([rows[0][1:].strip(), *rows[1:]]).strip()
|
|
3067
|
+
footer = block[-1]
|
|
3068
|
+
if "esc interrupt" in footer:
|
|
3069
|
+
running = True
|
|
3070
|
+
elif "Interactive ·" in footer or "· autopilot ·" in footer:
|
|
3071
|
+
running = False
|
|
3072
|
+
else:
|
|
3073
|
+
return None
|
|
3074
|
+
if text is None or running is None:
|
|
3075
|
+
return (None, None)
|
|
3076
|
+
composer = "clear" if not text else "wake" if _only_wake_text(text) else "busy"
|
|
3077
|
+
return (composer, running)
|
|
3078
|
+
|
|
3079
|
+
|
|
3016
3080
|
def _agent_running(target: str) -> Optional[bool]:
|
|
3017
3081
|
"""True while a keystroke would miss the idle composer.
|
|
3018
3082
|
|
|
@@ -3030,6 +3094,9 @@ def _agent_running(target: str) -> Optional[bool]:
|
|
|
3030
3094
|
lines = _capture_pane_lines(target)
|
|
3031
3095
|
if lines is None:
|
|
3032
3096
|
return None
|
|
3097
|
+
copilot = _copilot_frame(lines)
|
|
3098
|
+
if copilot is not None:
|
|
3099
|
+
return copilot[1]
|
|
3033
3100
|
if _is_opencode_pane(lines):
|
|
3034
3101
|
return _opencode_running(lines)
|
|
3035
3102
|
if _is_grok_pane(lines):
|
|
@@ -3080,6 +3147,9 @@ def _composer_state(target: str) -> Optional[str]:
|
|
|
3080
3147
|
lines = _capture_pane_lines(target)
|
|
3081
3148
|
if lines is None:
|
|
3082
3149
|
return None
|
|
3150
|
+
copilot = _copilot_frame(lines)
|
|
3151
|
+
if copilot is not None:
|
|
3152
|
+
return copilot[0]
|
|
3083
3153
|
if _is_opencode_pane(lines):
|
|
3084
3154
|
return _opencode_composer_state(lines)
|
|
3085
3155
|
if _is_grok_pane(lines):
|
|
@@ -14,14 +14,26 @@ A quota VALUE without its as_of coordinate is a rumour: --quota-used /
|
|
|
14
14
|
--resets without --quota-as-of are refused client-side (exit 2) before the
|
|
15
15
|
gateway's own 400. The pool NAME needs no coordinate (identifier, not a
|
|
16
16
|
measurement). --clear-until is the grok-returns flow (explicit null clears).
|
|
17
|
+
|
|
18
|
+
swarph roster sync [--peer PEER]... [--service-log PEER=PATH]...
|
|
19
|
+
[--state PATH] [--json]
|
|
20
|
+
|
|
21
|
+
card #1071: one quota pass over local tmux panes — parse each membrane's
|
|
22
|
+
usage display, PUT the roster on change, DM the commander on a crossing.
|
|
23
|
+
Runs as the quota-writer #417 service (its token via MESH_GATEWAY_TOKEN /
|
|
24
|
+
--token-file); at most one pane read per peer per 5 minutes.
|
|
17
25
|
"""
|
|
18
26
|
from __future__ import annotations
|
|
19
27
|
|
|
20
28
|
import argparse
|
|
21
29
|
import json
|
|
30
|
+
import os
|
|
31
|
+
import subprocess
|
|
22
32
|
import sys
|
|
23
33
|
import urllib.error
|
|
24
34
|
import urllib.request
|
|
35
|
+
from datetime import datetime, timezone
|
|
36
|
+
from pathlib import Path
|
|
25
37
|
|
|
26
38
|
# card #1061 role vocabulary — mirrors the gateway's _ROSTER_ROLES. A role
|
|
27
39
|
# the gateway does not know is refused here (exit 2) before its 400; when the
|
|
@@ -219,15 +231,199 @@ def _build_parser():
|
|
|
219
231
|
group.add_argument("--clear-until", action="store_true",
|
|
220
232
|
help="clear unavailable_until (peer is back)")
|
|
221
233
|
s.add_argument("--note", default=None)
|
|
234
|
+
sy = sub.add_parser("sync", help="one quota pass over local tmux panes "
|
|
235
|
+
"(card #1071): parse, PUT on change, "
|
|
236
|
+
"alert on crossing")
|
|
237
|
+
_add_global(sy)
|
|
238
|
+
sy.add_argument("--json", action="store_true",
|
|
239
|
+
help="print the raw sync summary")
|
|
240
|
+
sy.add_argument("--peer", action="append", default=None,
|
|
241
|
+
help="peer to read (repeatable; default: all local "
|
|
242
|
+
"tmux sessions)")
|
|
243
|
+
sy.add_argument("--service-log", action="append", default=None,
|
|
244
|
+
metavar="PEER=PATH",
|
|
245
|
+
help="provider log tail for the headless error path "
|
|
246
|
+
"(repeatable)")
|
|
247
|
+
sy.add_argument("--state", default=None, metavar="PATH",
|
|
248
|
+
help="state file override (default: "
|
|
249
|
+
"$XDG_STATE_HOME/swarph/quota_sync_state.json)")
|
|
222
250
|
return p
|
|
223
251
|
|
|
224
252
|
|
|
253
|
+
def _sync_state_path(explicit=None) -> Path:
|
|
254
|
+
"""$XDG_STATE_HOME/swarph/quota_sync_state.json, else ~/.local/state —
|
|
255
|
+
the watchdog dm-wake convention, one file over."""
|
|
256
|
+
if explicit:
|
|
257
|
+
return Path(explicit).expanduser()
|
|
258
|
+
root = os.environ.get("XDG_STATE_HOME", "").strip()
|
|
259
|
+
if root:
|
|
260
|
+
return Path(root) / "swarph" / "quota_sync_state.json"
|
|
261
|
+
return Path.home() / ".local" / "state" / "swarph" / "quota_sync_state.json"
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _load_sync_state(path: Path) -> dict:
|
|
265
|
+
try:
|
|
266
|
+
with Path(path).open(encoding="utf-8") as fp:
|
|
267
|
+
data = json.load(fp)
|
|
268
|
+
except (FileNotFoundError, OSError, ValueError):
|
|
269
|
+
return {}
|
|
270
|
+
return data if isinstance(data, dict) else {}
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _save_sync_state(path: Path, state: dict) -> None:
|
|
274
|
+
"""Atomic best-effort persist (temp sibling + os.replace, never raises)."""
|
|
275
|
+
path = Path(path)
|
|
276
|
+
try:
|
|
277
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
278
|
+
tmp = path.with_suffix(".tmp")
|
|
279
|
+
tmp.write_text(json.dumps(state), encoding="utf-8")
|
|
280
|
+
os.replace(tmp, path)
|
|
281
|
+
except OSError:
|
|
282
|
+
pass
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _local_tmux_peers() -> list:
|
|
286
|
+
"""Session names on this box. [] when tmux is missing — sync then covers
|
|
287
|
+
only --peer/--service-log peers instead of failing the whole pass."""
|
|
288
|
+
try:
|
|
289
|
+
out = subprocess.run(["tmux", "list-sessions", "-F", "#{session_name}"],
|
|
290
|
+
capture_output=True, timeout=10, text=True,
|
|
291
|
+
encoding="utf-8", errors="replace")
|
|
292
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
293
|
+
return []
|
|
294
|
+
if out.returncode != 0:
|
|
295
|
+
return []
|
|
296
|
+
return [ln.strip() for ln in (out.stdout or "").splitlines() if ln.strip()]
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _capture_peer_pane(peer: str):
|
|
300
|
+
"""One read-only full-height capture of the peer's active pane. Never
|
|
301
|
+
sends keys (ruling 6); None on any failure — the peer reads unknown."""
|
|
302
|
+
try:
|
|
303
|
+
out = subprocess.run(["tmux", "capture-pane", "-p", "-t", peer],
|
|
304
|
+
capture_output=True, timeout=10, text=True,
|
|
305
|
+
encoding="utf-8", errors="replace")
|
|
306
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
307
|
+
return None
|
|
308
|
+
if out.returncode != 0:
|
|
309
|
+
return None
|
|
310
|
+
return out.stdout or ""
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _parse_service_logs(values) -> dict:
|
|
314
|
+
"""['PEER=PATH', ...] -> {peer: tail text}. A missing/unreadable log is
|
|
315
|
+
skipped (its peer still gets the pane path), never fatal."""
|
|
316
|
+
logs = {}
|
|
317
|
+
for item in values or []:
|
|
318
|
+
if "=" not in item:
|
|
319
|
+
raise ValueError(f"--service-log takes PEER=PATH, got {item!r}")
|
|
320
|
+
peer, _, path = item.partition("=")
|
|
321
|
+
peer, path = peer.strip(), path.strip()
|
|
322
|
+
if not peer or not path:
|
|
323
|
+
raise ValueError(f"--service-log takes PEER=PATH, got {item!r}")
|
|
324
|
+
try:
|
|
325
|
+
with open(path, encoding="utf-8", errors="replace") as fp:
|
|
326
|
+
fp.seek(0, os.SEEK_END)
|
|
327
|
+
size = fp.tell()
|
|
328
|
+
fp.seek(max(0, size - 65536))
|
|
329
|
+
logs[peer] = fp.read()
|
|
330
|
+
except OSError:
|
|
331
|
+
continue
|
|
332
|
+
return logs
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _post_message(gateway: str, token: str, to_node: str, content: str) -> bool:
|
|
336
|
+
"""DM the commander as the quota-writer service. True on HTTP 2xx."""
|
|
337
|
+
from swarph_cli.commands.mesh import _require_absolute_gateway_url
|
|
338
|
+
_require_absolute_gateway_url(gateway)
|
|
339
|
+
data = json.dumps({"from_node": "quota-writer", "to_node": to_node,
|
|
340
|
+
"kind": "fyi", "content": content}).encode("utf-8")
|
|
341
|
+
req = urllib.request.Request(
|
|
342
|
+
f"{gateway}/messages", data=data, method="POST",
|
|
343
|
+
headers={"Authorization": f"Bearer {token}",
|
|
344
|
+
"Content-Type": "application/json"})
|
|
345
|
+
try:
|
|
346
|
+
with urllib.request.urlopen(req, timeout=10.0) as resp:
|
|
347
|
+
return 200 <= resp.status < 300
|
|
348
|
+
except (urllib.error.URLError, OSError):
|
|
349
|
+
return False
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _run_sync(args):
|
|
353
|
+
from swarph_cli import quota_sync as qs
|
|
354
|
+
token = _resolve(args)
|
|
355
|
+
if token is None:
|
|
356
|
+
return 2
|
|
357
|
+
gateway = args.gateway.rstrip("/")
|
|
358
|
+
try:
|
|
359
|
+
service_logs = _parse_service_logs(args.service_log)
|
|
360
|
+
except ValueError as exc:
|
|
361
|
+
print(f"swarph roster sync: {exc}", file=sys.stderr)
|
|
362
|
+
return 2
|
|
363
|
+
peers = list(args.peer or []) or _local_tmux_peers()
|
|
364
|
+
for peer in service_logs:
|
|
365
|
+
if peer not in peers:
|
|
366
|
+
peers.append(peer)
|
|
367
|
+
if not peers:
|
|
368
|
+
print("swarph roster sync: no peers (no --peer, no tmux sessions, "
|
|
369
|
+
"no --service-log)", file=sys.stderr)
|
|
370
|
+
return 1
|
|
371
|
+
|
|
372
|
+
def _get_roster():
|
|
373
|
+
status, payload = _http_get_json(f"{gateway}/roster", token)
|
|
374
|
+
if status < 200 or status >= 300 or not isinstance(payload, dict):
|
|
375
|
+
return []
|
|
376
|
+
rows = payload.get("roster")
|
|
377
|
+
return rows if isinstance(rows, list) else []
|
|
378
|
+
|
|
379
|
+
failures = []
|
|
380
|
+
|
|
381
|
+
def _put_roster(peer, body):
|
|
382
|
+
status, payload = _http_put_json(
|
|
383
|
+
f"{gateway}/roster/{peer}", body, token)
|
|
384
|
+
if status < 200 or status >= 300:
|
|
385
|
+
failures.append(peer)
|
|
386
|
+
|
|
387
|
+
def _send_alert(peer, text):
|
|
388
|
+
_post_message(gateway, token, "commander",
|
|
389
|
+
f"quota [{peer}]: {text}")
|
|
390
|
+
|
|
391
|
+
state_path = _sync_state_path(args.state)
|
|
392
|
+
summary = qs.sync_once(
|
|
393
|
+
peers=peers, capture=_capture_peer_pane, get_roster=_get_roster,
|
|
394
|
+
put_roster=_put_roster, send_alert=_send_alert,
|
|
395
|
+
service_logs=service_logs,
|
|
396
|
+
state=_load_sync_state(state_path),
|
|
397
|
+
now=datetime.now(timezone.utc))
|
|
398
|
+
_save_sync_state(state_path, summary.get("state", {}))
|
|
399
|
+
if args.json:
|
|
400
|
+
print(json.dumps(summary, indent=2))
|
|
401
|
+
else:
|
|
402
|
+
for peer, entry in summary.get("peers", {}).items():
|
|
403
|
+
if entry.get("skipped"):
|
|
404
|
+
print(f" {peer}: skipped ({entry['skipped']})")
|
|
405
|
+
continue
|
|
406
|
+
bits = [entry.get("record", "?")]
|
|
407
|
+
if entry.get("put"):
|
|
408
|
+
bits.append("PUT")
|
|
409
|
+
for threshold in entry.get("alerts", []):
|
|
410
|
+
bits.append(f"ALERT {threshold}")
|
|
411
|
+
print(f" {peer}: {' '.join(bits)}")
|
|
412
|
+
if failures:
|
|
413
|
+
print(f"swarph roster sync: PUT failed for: {', '.join(failures)}",
|
|
414
|
+
file=sys.stderr)
|
|
415
|
+
return 1
|
|
416
|
+
return 0
|
|
417
|
+
|
|
418
|
+
|
|
225
419
|
def run_roster(argv):
|
|
226
420
|
parser = _build_parser()
|
|
227
421
|
args = parser.parse_args(argv)
|
|
228
422
|
try:
|
|
229
423
|
if args.command == "set":
|
|
230
424
|
return _run_set(args)
|
|
425
|
+
if args.command == "sync":
|
|
426
|
+
return _run_sync(args)
|
|
231
427
|
if args.command is None:
|
|
232
428
|
return _run_list(args)
|
|
233
429
|
parser.error(f"unknown command: {args.command}")
|