swarph-cli 0.72.25__tar.gz → 0.72.26__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.25/src/swarph_cli.egg-info → swarph_cli-0.72.26}/PKG-INFO +1 -1
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/pyproject.toml +1 -1
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/__init__.py +1 -1
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/mesh.py +11 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/quota_sync.py +133 -13
- {swarph_cli-0.72.25 → swarph_cli-0.72.26/src/swarph_cli.egg-info}/PKG-INFO +1 -1
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli.egg-info/SOURCES.txt +1 -0
- swarph_cli-0.72.26/tests/test_1219_egress_probe.py +261 -0
- swarph_cli-0.72.26/tests/test_1221_quota_windows.py +114 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_mesh_command.py +40 -0
- swarph_cli-0.72.25/tests/test_1219_egress_probe.py +0 -109
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/LICENSE +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/README.md +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/setup.cfg +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/__main__.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/bench/__init__.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/bench/backends.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/bench/claude_cli.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/bench/codex_cli.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/bench/pack.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/bench/prices.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/bench/providers.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/bench/quality.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/bench/refresh_prices.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/bench/rules.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/bench/runner.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/bench/signal_score.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/bench/validate.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/caller.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/capture/__init__.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/capture/harden.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/capture/lineage.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/capture/liveness.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/capture/manifest.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/capture/paths.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/capture/verify.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/cell.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/chain_token.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/channel.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/__init__.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/_content.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/_display.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/_gateway_client.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/add.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/bench.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/board.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/board_merge_check.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/brain.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/brain_ask.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/cell.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/cell_probe.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/cell_selfcheck.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/channel.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/chat.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/codegraph.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/codegraph_hook.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/codex_hook_output.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/codex_waker.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/compress.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/daemon.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/dreaming.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/event.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/gateway.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/gh_route.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/group.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/guide.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/highlight.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/hook_interpreter.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/hook_output.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/hooks.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/import_session.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/init.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/install_codex_hooks.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/install_hook.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/install_multiplexer.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/install_opencode_plugin.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/install_postcompact_hook.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/install_wake_hook.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/lane.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/mcp_server.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/memory.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/memory_emit_hook.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/memory_sync.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/monitor.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/okf_links.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/onboard.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/peer_reply_drain.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/postcompact_hook_output.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/protocol_handler.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/ratify.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/roster.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/rowclear_hook.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/schedule.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/security.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/service.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/spawn.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/sweep.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/timeline.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/triage.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/version.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/wake_hook_output.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/watchdog.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/compress/__init__.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/compress/levers.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/compress/marker.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/compress/verify.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/console_safe.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/delivery_queue.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/dm_frame.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/dreaming/__init__.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/dreaming/candidates.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/dreaming/clone.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/dreaming/enrich.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/dreaming/index_check.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/dreaming/notify.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/dreaming/organize.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/dreaming/probes.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/dreaming/report.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/dreaming/run.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/dreaming/slm.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/dreaming/transcripts.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/dreaming/verify.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/gateway/__init__.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/gateway/feature_registry.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/gateway/lanes_control.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/gateway/schema.sql +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/gateway/server.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/gateway/services_control.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/gateway_default.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/gh_identity.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/guide/GUIDE.md +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/identity.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/launchd/swarph-codex-outbox.plist +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/launchd/swarph-codex-outbox.timer.plist +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/launchd/swarph-monitor.plist +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/launchd/swarph-watchdog.plist +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/launchd/swarph-watchdog.timer.plist +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/launchd_render.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/main.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/multiplexer.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/operator/__init__.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/operator/__main__.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/operator/api.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/operator/cli.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/operator/config.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/operator/fmt.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/orphan_daemons.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/pane_probe.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/parsers/__init__.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/parsers/claude.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/payloads/opencode/plugin.js +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/payloads/postcompact/_shim.cmd +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/payloads/postcompact/postcompact-recall-grok.sh +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/payloads/postcompact/postcompact-recall.sh +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/payloads/postcompact/posttooluse-emit-antigravity.sh +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/payloads/postcompact/posttooluse-emit-claude.sh +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/payloads/postcompact/posttooluse-emit-grok.sh +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/payloads/postcompact/posttooluse-emit.sh +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/payloads/postcompact/posttooluse-recall.sh +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/payloads/postcompact/precompact-flag.sh +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/payloads/postcompact/preinvocation-recall-antigravity.sh +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/payloads/postcompact/sessionstart-recall.sh +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/peer_executor.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/peer_executor_windows.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/peer_reconciliation.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/peer_reply_outbox.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/peer_service_host.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/peer_service_payload.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/peer_service_reply.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/peer_staging.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/query_filters.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/rowclear.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/scripts/__init__.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/scripts/dm_notify_filter.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/scripts/ensure_monitor.sh +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/scripts/install_codex_waker_windows.ps1 +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/scripts/install_monitor_task_windows.ps1 +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/scripts/wake_watchdog.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/scrub.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/service/__init__.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/service/app.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/service/providers.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/session_bridge.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/skills/i-have-adhd/LICENSE +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/skills/i-have-adhd/SKILL.md +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/source_text.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/stall_alert.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/codex-waker.default +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/peer-reply-drain.default +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-codex-outbox@.service +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-codex-outbox@.timer +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-codex-waker@.service +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-codex-waker@.timer +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-monitor-reexec.path +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-monitor-reexec.service +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-monitor@.service +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-peer-reply-drain@.service +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-peer-reply-drain@.timer +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-stuck-sweep.service +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-stuck-sweep.timer +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-triage.service +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-triage.timer +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/timeline_paths.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/tokens.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/triage/__init__.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/triage/backend.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/triage/job.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/triage/owners.json +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/triage/router.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/triage/rows.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/triage/sensor.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli.egg-info/entry_points.txt +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli.egg-info/requires.txt +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli.egg-info/top_level.txt +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1006_board_publisher.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1006_session_board.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1010_strand_only.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1013_spawn_channel.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1017_muse_tui.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1050_surrogate_wake.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1052_opencode_deaf.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1059_watchdog_unread_env.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1061_roster_cli.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1064_stuck_sweep.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1065_release.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1066_quiet_wakes.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1068_structured_accept_cli.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1071_quota_sync.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1073_triage.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1073_triage_board.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1073_wheel_install.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1075_task_cli.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1083_openai_decisions.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1144_cell_yaml_github.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1197_commander_taps.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1203_swarph_me_refusal.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1206_offer_question.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1214_egress.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_124_register_merge.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_126_pending_from_complete.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_132_ensure_monitor_state_dir.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_138_memory_navigate.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_139_tool_probe.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_183_card_anchor.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_184_notify_honesty.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_201_win32_bash_refusal.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_243_onboard_per_peer_token.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_307_task4_ask_reply.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_312_codegraph_fuzzy_verb.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_319_refusal_exit_codes.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_323_mistral_backend.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_332_report_organize_findings.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_356_query_filters.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_397_gh_identity_router.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_397_router_preflight.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_402_one_identity_resolver.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_432_rowclear.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_467b_onboard_operator_credential.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_472_register_caller_binding.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_484_codex_queue_sink.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_523_guide.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_524_conftest_hermeticity.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_532_accept_flag.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_535_version_capability.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_538_signal_bench.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_546_default_gateway.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_547_verbs_from_registry.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_548_gateway_remainder.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_552_codegraph_gateway.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_555_relay_row_shape.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_556_relay_freshness.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_559_edges_truncated.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_564_bootstrap_first_peer.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_578_no_machine_specific_host_defaults.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_579_register_bootstrap_message.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_591_cli_step_graph.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_644_monitor_supervision.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_648_install_unit.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_654_reexec.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_656_owned_slm.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_684_dreaming_verb.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_689_pkg_version_order.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_715_grok_wake.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_716_timeline_one_path.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_723_grok_0251.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_724_ps1_ascii_or_bom.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_725_stdio_reconfigure.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_726_grok_limit_screen.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_729_arm_check_delivery_gap.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_729_dm_once.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_729_wake_watchdog.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_734_dreaming_enrich_degrade.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_735_dm_mcp_tools.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_739_wake_writer.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_740_edit_project.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_762_cards_edit_priority.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_764_enrich_collapse.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_801_confirm_flow_cli.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_802_stage_history.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_825_codegraph_hook_noise.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_825_install_posttooluse_only.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_825_remaining_cap_shred_bind.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_829_counterfactual_column.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_830_hook_binding_version.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_84_cli_labels.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_866_doctrine_guide.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_872_identity_refuses_not_warns.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_874_source_text.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_888_opencode_data_dir_can_fail.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_937_dreaming_notify.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_938_organize_line_limit.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_941_bench_backends.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_941_harness_342.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_945_same_line_pairing.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_955_prompt_audit.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_957_auth_http.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_959_venv_interpreter.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_960_channel.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_960_ensure_monitor_heartbeat.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_961_cursor_grok_footer.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_961_loud_stop.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_963_plugin_manifest.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_964_channel_cell.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_965_mesh_wait.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_975_channel_hop.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_add_security_gate.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_artifact_add.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_artifact_hash.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_artifact_lib.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_artifact_mcp.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_artifact_skill.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_artifact_tool.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_artifact_uri.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_bench_backends.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_bench_claude_cli.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_bench_codex_cli.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_bench_command.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_bench_pack.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_bench_prices.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_bench_quality.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_bench_runner.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_bench_validate.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_board_card_thread.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_board_command.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_board_merge_check.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_board_obligation_close.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_brain_ask_command.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_brain_ask_env_diagnosis.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_brain_ask_identity_explicitness.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_brain_command.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_caller_meta_guard.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_capture_lineage.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_capture_liveness.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_capture_manifest.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_capture_paths.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_capture_security.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_cell_command_dispatch.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_cell_harden.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_cell_loader.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_cell_selfcheck.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_cell_verify.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_chain_token.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_channel_command.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_channel_poll_never_breaks_dms.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_channel_post.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_channel_wake_policy_filter.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_chat_command.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_claude_parser.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_claude_tmux_template.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_codegraph_caller_scope.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_codegraph_command.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_codegraph_hook.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_codegraph_mcp.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_codex_waker.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_compress_caller_convention.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_compress_command.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_compress_levers.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_compress_marker.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_compress_verify.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_console_encoding_e2e.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_content_composition.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_copilot_membrane.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_copilot_pane_reader.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_cursor_membrane.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_daemon_command.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_daemon_console_encoding.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_daemon_delivery.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_delivery_queue.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_display_sanitize.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_ensure_monitor_no_peer_default.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_event_emit.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_feature_to_uri.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_gateway_brain.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_gateway_client.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_gateway_command.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_gateway_message_idempotency.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_gh_route_doctor_mesh_name.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_group_command.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_guide_commands_parse.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_guide_selfcheck.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_heartbeat_check.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_highlight_command.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_hook_output.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_hooks_activity_marker.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_hooks_add.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_hooks_bundle.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_hooks_init.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_hooks_legacy_generation_removal.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_hooks_lifecycle.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_hooks_merge.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_hooks_verify.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_hooks_win32_bash_wrapper.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_hooks_win32_command_path.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_import_command.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_inbox_identity_provenance.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_init_command.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_init_provider_menu.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_install_codex_hooks.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_install_hook.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_install_multiplexer_command.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_install_postcompact_hook.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_install_wake_hook.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_lane_command.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_launchd_units.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_main.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_main_unknown_verb_guard.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_mcp_missing_hint.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_mcp_server.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_memory_command.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_memory_emit_snippet.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_memory_gateway.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_memory_sync.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_memory_sync_membrane.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_mesh_inbox_mark_read.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_mesh_inbox_sanitize.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_mesh_originate_thread_623.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_mesh_send_unknown_peer.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_mesh_sidecar.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_mesh_sidecar_cursor_decoupled.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_mesh_sidecar_muse_sink.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_mesh_sidecar_wake_survives_read.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_meta_edge_identity.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_monitor_channel_poll.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_monitor_cli.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_monitor_daemon_collision.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_monitor_self_name_verification.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_monitor_state_split.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_multiplexer.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_muse_membrane.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_muse_own_cli.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_obligation_close_is_surfaced.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_okf_links.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_onboard_command.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_opencode_membrane.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_opencode_plugin.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_operator_me.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_orphan_daemons.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_packaged_artifacts_exist.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_packaged_skills.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_peer_executor.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_peer_executor_windows.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_peer_reply_drain_command.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_peer_reply_drain_supervisor.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_peer_reply_outbox.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_peer_reply_transport.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_peer_service_e2e.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_peer_service_host.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_peer_service_payload.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_peer_service_reply.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_postcompact_recall.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_process_liveness.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_protocol_handler.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_providers_isolation.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_ratify_command.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_readme_claims_no_gateway_default.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_repl_caller_convention.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_schedule_command.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_schedule_create_contract.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_scrub.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_security.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_service_app.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_service_command.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_service_providers.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_session_bridge_inject.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_session_bridge_probe.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_session_bridge_resolve.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_session_path_role_gate.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_shipped_units_match_the_cli.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_smoke_chat.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_smoke_one_shot.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_smoke_phase_5_5.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_spawn_base_hoist.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_spawn_command.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_spawn_identity_stamp.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_spawn_live_pin.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_spawn_mitosis_lineage.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_spawn_tmux_session.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_spawn_windows_relaunch.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_stall_alert.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_subprocess_decode_encoding.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_timeline_command.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_tmux_wake_runstate_defer.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_tmux_wake_session_reanchor.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_tmux_wake_stale.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_tmux_wake_submit_verify.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_token_file_one_parser.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_token_precedence_explicit_first.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_unified_token_resolver.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_version_command.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_version_consistency.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_vibe_membrane.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_watchdog.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_watchdog_dm_wake.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_watchdog_dm_wake_wiring.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_watchdog_emit_health.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_watchdog_install_flags.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_watchdog_model_rung.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_watchdog_stale_peers.py +0 -0
- {swarph_cli-0.72.25 → swarph_cli-0.72.26}/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.26
|
|
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
|
|
@@ -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.26"
|
|
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" }
|
|
@@ -97,6 +97,15 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
97
97
|
default=[],
|
|
98
98
|
help="extra recipient (repeatable). from_node in this list is refused.",
|
|
99
99
|
)
|
|
100
|
+
send.add_argument(
|
|
101
|
+
"--reply-to",
|
|
102
|
+
type=int,
|
|
103
|
+
default=None,
|
|
104
|
+
metavar="ID",
|
|
105
|
+
help="id of the message this one answers, sent as reply_to (card #1224). "
|
|
106
|
+
"The gateway refuses an id that does not exist. It binds nothing "
|
|
107
|
+
"by itself and does NOT set thread_id.",
|
|
108
|
+
)
|
|
100
109
|
add_content_args(send)
|
|
101
110
|
_add_common(send)
|
|
102
111
|
|
|
@@ -680,6 +689,8 @@ def _run_send(args: argparse.Namespace) -> int:
|
|
|
680
689
|
}
|
|
681
690
|
if cc:
|
|
682
691
|
body["cc"] = cc
|
|
692
|
+
if getattr(args, "reply_to", None) is not None:
|
|
693
|
+
body["reply_to"] = args.reply_to
|
|
683
694
|
thread_arg = getattr(args, "thread", None)
|
|
684
695
|
if thread_arg:
|
|
685
696
|
if thread_arg == "new":
|
|
@@ -12,10 +12,13 @@ or null, and never clears a prior quota or block.
|
|
|
12
12
|
"""
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
|
+
import logging
|
|
15
16
|
import re
|
|
16
17
|
from datetime import datetime, timedelta, timezone
|
|
17
18
|
from typing import Callable, Optional
|
|
18
19
|
|
|
20
|
+
log = logging.getLogger(__name__)
|
|
21
|
+
|
|
19
22
|
READ_COOLDOWN = timedelta(minutes=5)
|
|
20
23
|
BLOCK_HOLD = timedelta(minutes=5)
|
|
21
24
|
|
|
@@ -31,6 +34,16 @@ _LOG_LIMIT_RE = re.compile(
|
|
|
31
34
|
r"usage limit[^\n]*?try again at\s+(.+?)\s*$",
|
|
32
35
|
re.IGNORECASE | re.MULTILINE)
|
|
33
36
|
_RATE_EXCEEDED_RE = re.compile(r"rate limits? exceeded", re.IGNORECASE)
|
|
37
|
+
# card #1221: Claude Code /usage, the 2026-10-19 22:42Z capture. The two
|
|
38
|
+
# runway windows are "Current session" and "Current week (all models)".
|
|
39
|
+
# "Current week (Fable)" is the same weekly reset for one model, so it is
|
|
40
|
+
# not a second window. The gateway ranks on used_pct, resets_at and as_of.
|
|
41
|
+
_CLAUDE_USAGE_RE = re.compile(
|
|
42
|
+
r"^Current (session|week \(all models\)): "
|
|
43
|
+
r"([0-9]+(?:\.[0-9]+)?)% used \u00b7 resets "
|
|
44
|
+
r"(?:([A-Z][a-z]{2} \d{1,2}), )?(\d{1,2}(?::\d{2})?[ap]m) \(UTC\)\s*$",
|
|
45
|
+
re.MULTILINE,
|
|
46
|
+
)
|
|
34
47
|
|
|
35
48
|
|
|
36
49
|
def _iso(dt: datetime) -> str:
|
|
@@ -39,40 +52,136 @@ def _iso(dt: datetime) -> str:
|
|
|
39
52
|
return dt.isoformat()
|
|
40
53
|
|
|
41
54
|
|
|
55
|
+
def _clock(clock: str) -> Optional[datetime]:
|
|
56
|
+
"""`4:59pm` or the on-the-hour form `5pm`."""
|
|
57
|
+
norm = clock[:-2] + clock[-2:].upper()
|
|
58
|
+
try:
|
|
59
|
+
return datetime.strptime(norm, "%I:%M%p" if ":" in norm else "%I%p")
|
|
60
|
+
except ValueError:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _claude_reset_iso(date: Optional[str], clock: str, as_of: str) -> Optional[str]:
|
|
65
|
+
"""A dated reset uses as_of's year. A bare clock is the next time after as_of."""
|
|
66
|
+
try:
|
|
67
|
+
anchor = datetime.fromisoformat(as_of.replace("Z", "+00:00"))
|
|
68
|
+
except ValueError:
|
|
69
|
+
return None
|
|
70
|
+
if anchor.tzinfo is None:
|
|
71
|
+
return None
|
|
72
|
+
anchor = anchor.astimezone(timezone.utc)
|
|
73
|
+
clock_dt = _clock(clock)
|
|
74
|
+
if clock_dt is None:
|
|
75
|
+
return None
|
|
76
|
+
if not date:
|
|
77
|
+
candidate = anchor.replace(
|
|
78
|
+
hour=clock_dt.hour, minute=clock_dt.minute,
|
|
79
|
+
second=0, microsecond=0)
|
|
80
|
+
if candidate <= anchor:
|
|
81
|
+
candidate += timedelta(days=1)
|
|
82
|
+
return candidate.isoformat()
|
|
83
|
+
norm = clock[:-2] + clock[-2:].upper()
|
|
84
|
+
try:
|
|
85
|
+
parsed = datetime.strptime(
|
|
86
|
+
f"{date} {norm} {anchor.year}",
|
|
87
|
+
"%b %d " + ("%I:%M%p" if ":" in norm else "%I%p") + " %Y",
|
|
88
|
+
)
|
|
89
|
+
except ValueError:
|
|
90
|
+
return None
|
|
91
|
+
parsed = parsed.replace(tzinfo=timezone.utc)
|
|
92
|
+
if parsed < anchor - timedelta(days=1):
|
|
93
|
+
parsed = parsed.replace(year=anchor.year + 1)
|
|
94
|
+
return parsed.isoformat()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _window_rows(text: str, as_of: str) -> list:
|
|
98
|
+
"""Last session line and last weekly-all-models line, or neither.
|
|
99
|
+
|
|
100
|
+
A pane can repeat a name or hold an older /usage display above a newer
|
|
101
|
+
one. Counting rows would send two session windows, or stamp the stale
|
|
102
|
+
display as current, which is the 5h mask card #1217 exists to stop.
|
|
103
|
+
The set is the last line of each name, and it is sent only when both
|
|
104
|
+
names parsed.
|
|
105
|
+
"""
|
|
106
|
+
by_name = {}
|
|
107
|
+
for match in _CLAUDE_USAGE_RE.finditer(text or ""):
|
|
108
|
+
reset = _claude_reset_iso(match.group(3), match.group(4), as_of)
|
|
109
|
+
if reset is None:
|
|
110
|
+
continue
|
|
111
|
+
by_name[match.group(1)] = {"used_pct": float(match.group(2)),
|
|
112
|
+
"resets_at": reset,
|
|
113
|
+
"as_of": as_of}
|
|
114
|
+
session = by_name.get("session")
|
|
115
|
+
weekly = by_name.get("week (all models)")
|
|
116
|
+
if session is not None and weekly is not None:
|
|
117
|
+
return [session, weekly]
|
|
118
|
+
if session is None and weekly is None:
|
|
119
|
+
return []
|
|
120
|
+
found = "session" if session is not None else "week (all models)"
|
|
121
|
+
missing = "weekly" if session is not None else "session"
|
|
122
|
+
log.warning(
|
|
123
|
+
"quota windows dropped: parsed %s only, %s did not parse; "
|
|
124
|
+
"sending no quota_windows",
|
|
125
|
+
found, missing)
|
|
126
|
+
return []
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _with_windows(record: Optional[dict], windows: list) -> Optional[dict]:
|
|
130
|
+
if record is None:
|
|
131
|
+
return None
|
|
132
|
+
if windows:
|
|
133
|
+
record = dict(record)
|
|
134
|
+
record["windows"] = windows
|
|
135
|
+
return record
|
|
136
|
+
|
|
137
|
+
|
|
42
138
|
def parse_pane_capture(text: str, as_of: str):
|
|
43
139
|
"""One read-only pane capture -> a usage record, or None (unknown).
|
|
44
140
|
|
|
45
141
|
Self-selecting: the codex hit-limit line beats its own heads-up bound;
|
|
46
142
|
a grok modal and a mistral rate error are blocked without figures;
|
|
47
|
-
anything else (Claude
|
|
143
|
+
anything else (a Claude cell transcript, agy labels, empty captures)
|
|
144
|
+
is None. A Claude Code /usage display keeps two windows: the session
|
|
145
|
+
and the weekly all-models line (card #1221). The per-model week line
|
|
146
|
+
is not a second runway window.
|
|
48
147
|
"""
|
|
49
148
|
text = text or ""
|
|
149
|
+
windows = _window_rows(text, as_of)
|
|
50
150
|
low = text.lower()
|
|
51
151
|
# The mistral provider error surfaces in pane output but is classified
|
|
52
152
|
# source 'error' per ruling 2 — it is an API error, not a usage display.
|
|
53
153
|
if _RATE_EXCEEDED_RE.search(text):
|
|
54
|
-
return
|
|
55
|
-
|
|
154
|
+
return _with_windows(
|
|
155
|
+
{"used_pct": None, "credits": None, "reset_at": None,
|
|
156
|
+
"blocked": True, "source": "error", "as_of": as_of}, windows)
|
|
56
157
|
if "buy more credits" in low and "try again" in low:
|
|
57
|
-
return
|
|
58
|
-
|
|
158
|
+
return _with_windows(
|
|
159
|
+
{"used_pct": None, "credits": None, "reset_at": None,
|
|
160
|
+
"blocked": True, "source": "pane", "as_of": as_of}, windows)
|
|
59
161
|
hit = _CODEX_HIT_RE.search(text)
|
|
60
162
|
if hit:
|
|
61
163
|
# A trailing period is sentence punctuation, not timestamp.
|
|
62
164
|
reset = hit.group(1).strip().rstrip(".").strip() or None
|
|
63
|
-
return
|
|
64
|
-
|
|
165
|
+
return _with_windows(
|
|
166
|
+
{"used_pct": 100, "credits": None, "reset_at": reset,
|
|
167
|
+
"blocked": True, "source": "pane", "as_of": as_of}, windows)
|
|
65
168
|
bound = _CODEX_BOUND_RE.search(text)
|
|
66
169
|
if bound:
|
|
67
170
|
# A bound, not a measurement: used_pct stays null (ruling 2).
|
|
68
|
-
return
|
|
69
|
-
|
|
70
|
-
|
|
171
|
+
return _with_windows(
|
|
172
|
+
{"used_pct": None, "credits": None, "reset_at": None,
|
|
173
|
+
"blocked": False, "source": "pane", "as_of": as_of,
|
|
174
|
+
"bound_left_pct": float(bound.group(1))}, windows)
|
|
71
175
|
copilot = _COPILOT_RE.search(text)
|
|
72
176
|
if copilot:
|
|
73
|
-
return
|
|
74
|
-
|
|
75
|
-
|
|
177
|
+
return _with_windows(
|
|
178
|
+
{"used_pct": None, "credits": float(copilot.group(1)),
|
|
179
|
+
"reset_at": None, "blocked": False, "source": "pane",
|
|
180
|
+
"as_of": as_of}, windows)
|
|
181
|
+
if windows:
|
|
182
|
+
return {"used_pct": None, "credits": None, "reset_at": None,
|
|
183
|
+
"blocked": False, "source": "pane", "as_of": as_of,
|
|
184
|
+
"windows": windows}
|
|
76
185
|
return None
|
|
77
186
|
|
|
78
187
|
|
|
@@ -111,6 +220,17 @@ def decide_put(prior: Optional[dict], record: Optional[dict],
|
|
|
111
220
|
used = record.get("used_pct")
|
|
112
221
|
if used is not None and used != prior.get("quota_used_pct"):
|
|
113
222
|
body["quota_used_pct"] = used
|
|
223
|
+
windows = []
|
|
224
|
+
for window in record.get("windows") or []:
|
|
225
|
+
pct = window.get("used_pct")
|
|
226
|
+
reset = window.get("resets_at")
|
|
227
|
+
if pct is None or not reset:
|
|
228
|
+
continue
|
|
229
|
+
windows.append({"used_pct": float(pct),
|
|
230
|
+
"resets_at": str(reset),
|
|
231
|
+
"as_of": window.get("as_of") or as_of})
|
|
232
|
+
if windows and windows != (prior.get("quota_windows") or []):
|
|
233
|
+
body["quota_windows"] = windows
|
|
114
234
|
if record.get("blocked"):
|
|
115
235
|
reset = record.get("reset_at")
|
|
116
236
|
until = reset if reset else _iso(now + BLOCK_HOLD)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: swarph-cli
|
|
3
|
-
Version: 0.72.
|
|
3
|
+
Version: 0.72.26
|
|
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
|
|
@@ -235,6 +235,7 @@ tests/test_1203_swarph_me_refusal.py
|
|
|
235
235
|
tests/test_1206_offer_question.py
|
|
236
236
|
tests/test_1214_egress.py
|
|
237
237
|
tests/test_1219_egress_probe.py
|
|
238
|
+
tests/test_1221_quota_windows.py
|
|
238
239
|
tests/test_124_register_merge.py
|
|
239
240
|
tests/test_126_pending_from_complete.py
|
|
240
241
|
tests/test_132_ensure_monitor_state_dir.py
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""card #1219 / #1225 — runtime egress probe.
|
|
2
|
+
|
|
3
|
+
The probe runs under bwrap --unshare-net. An unlisted connect fails it.
|
|
4
|
+
A host that cannot prove isolation exits with isolation-unproven.
|
|
5
|
+
On main this file's first assertion fails: tests/egress_probe.py is absent.
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
15
|
+
PROBE = ROOT / "tests" / "egress_probe.py"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _run(args, timeout=60, **env):
|
|
19
|
+
assert PROBE.is_file(), "runtime egress probe is not in the tree"
|
|
20
|
+
merged = os.environ.copy()
|
|
21
|
+
merged.update(env)
|
|
22
|
+
return subprocess.run(
|
|
23
|
+
[sys.executable, str(PROBE), *args],
|
|
24
|
+
capture_output=True, text=True, env=merged, timeout=timeout)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.mark.skipif(sys.platform != "linux", reason="bwrap --unshare-net is Linux")
|
|
28
|
+
def test_os_system_curl_is_an_unlisted_connect():
|
|
29
|
+
out = _run(["--mutant", "os-system-curl"])
|
|
30
|
+
assert out.returncode == 1, out.stderr
|
|
31
|
+
assert "203.0.113.2" in out.stderr
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.mark.skipif(sys.platform != "linux", reason="bwrap --unshare-net is Linux")
|
|
35
|
+
def test_absolute_curl_is_an_unlisted_connect():
|
|
36
|
+
out = _run(["--mutant", "abs-curl"])
|
|
37
|
+
assert out.returncode == 1, out.stderr
|
|
38
|
+
assert "203.0.113.3" in out.stderr
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.mark.skipif(sys.platform != "linux", reason="bwrap --unshare-net is Linux")
|
|
42
|
+
def test_asyncio_ssh_is_an_unlisted_connect():
|
|
43
|
+
out = _run(["--mutant", "asyncio-ssh"])
|
|
44
|
+
assert out.returncode == 1, out.stderr
|
|
45
|
+
assert "203.0.113.4" in out.stderr
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_missing_bwrap_fails_closed_with_a_named_reason():
|
|
49
|
+
out = _run(["--isolation"], EGRESS_PROBE_BWRAP="/no/such/bwrap")
|
|
50
|
+
assert out.returncode == 2, out.stderr
|
|
51
|
+
assert "isolation-unproven: bwrap missing" in out.stderr
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.mark.skipif(sys.platform != "linux", reason="bwrap --unshare-net is Linux")
|
|
55
|
+
def test_ipv6_literal_connect_is_caught():
|
|
56
|
+
out = _run(["--mutant", "ipv6"])
|
|
57
|
+
assert out.returncode == 1, out.stderr
|
|
58
|
+
assert "2001:db8::1" in out.stderr
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.skipif(sys.platform != "linux", reason="bwrap --unshare-net is Linux")
|
|
62
|
+
def test_non_isolating_bwrap_shim_exits_2(tmp_path):
|
|
63
|
+
shim = tmp_path / "bwrap"
|
|
64
|
+
shim.write_text(
|
|
65
|
+
"#!/bin/sh\n"
|
|
66
|
+
"while [ $# -gt 0 ]; do\n"
|
|
67
|
+
" case \"$1\" in\n"
|
|
68
|
+
" *strace) break ;;\n"
|
|
69
|
+
" esac\n"
|
|
70
|
+
" shift\n"
|
|
71
|
+
"done\n"
|
|
72
|
+
"exec \"$@\"\n",
|
|
73
|
+
encoding="utf-8")
|
|
74
|
+
shim.chmod(0o700)
|
|
75
|
+
out = _run(["--isolation"], EGRESS_PROBE_BWRAP=str(shim))
|
|
76
|
+
assert out.returncode == 2, out.stderr
|
|
77
|
+
assert "isolation-unproven:" in out.stderr
|
|
78
|
+
assert "ENETUNREACH" in out.stderr
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@pytest.mark.skipif(sys.platform != "linux", reason="bwrap --unshare-net is Linux")
|
|
82
|
+
def test_planted_unlisted_connect_in_src_fails_the_probe(tmp_path):
|
|
83
|
+
src = tmp_path / "src" / "swarph_cli"
|
|
84
|
+
src.mkdir(parents=True)
|
|
85
|
+
(src / "__init__.py").write_text("", encoding="utf-8")
|
|
86
|
+
# Fixed mutant, not external input. The test reaches it by calling leak().
|
|
87
|
+
(src / "stall_alert.py").write_text(
|
|
88
|
+
"import os\n"
|
|
89
|
+
"def leak():\n"
|
|
90
|
+
" os.system('curl --max-time 1 -s -o /dev/null "
|
|
91
|
+
"http://203.0.113.50/')\n",
|
|
92
|
+
encoding="utf-8")
|
|
93
|
+
tests = tmp_path / "tests"
|
|
94
|
+
tests.mkdir()
|
|
95
|
+
(tests / "test_zz_leak.py").write_text(
|
|
96
|
+
"import sys\n"
|
|
97
|
+
"from pathlib import Path\n"
|
|
98
|
+
"sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'src'))\n"
|
|
99
|
+
"from swarph_cli.stall_alert import leak\n"
|
|
100
|
+
"def test_leak():\n"
|
|
101
|
+
" leak()\n",
|
|
102
|
+
encoding="utf-8")
|
|
103
|
+
assert PROBE.is_file(), "runtime egress probe is not in the tree"
|
|
104
|
+
sys.path.insert(0, str(PROBE.parent))
|
|
105
|
+
import egress_probe
|
|
106
|
+
assert hasattr(egress_probe, "run_pytest")
|
|
107
|
+
# Empty allowlist: the planted 203.0.113.50 literal is unlisted.
|
|
108
|
+
code = egress_probe.run_pytest(tmp_path, ["tests/test_zz_leak.py"], set())
|
|
109
|
+
assert code == 1, code
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# The four lines run_pytest uses to refuse a pytest run whose netns was
|
|
113
|
+
# not proven. Mutant M4 deletes them. Fixed source, not external input.
|
|
114
|
+
_ISOLATION_GUARD = (
|
|
115
|
+
" code = prove_isolation()\n"
|
|
116
|
+
" if code != 0:\n"
|
|
117
|
+
" print(\"pytest isolation was not proven\", file=sys.stderr)\n"
|
|
118
|
+
" return code\n"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _shim(path: Path) -> None:
|
|
123
|
+
# Drops every bwrap flag and execs strace on the host network.
|
|
124
|
+
path.write_text(
|
|
125
|
+
"#!/bin/sh\n"
|
|
126
|
+
"while [ $# -gt 0 ]; do\n"
|
|
127
|
+
" case \"$1\" in\n"
|
|
128
|
+
" *strace) break ;;\n"
|
|
129
|
+
" esac\n"
|
|
130
|
+
" shift\n"
|
|
131
|
+
"done\n"
|
|
132
|
+
"exec \"$@\"\n",
|
|
133
|
+
encoding="utf-8")
|
|
134
|
+
path.chmod(0o700)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@pytest.mark.skipif(sys.platform != "linux", reason="bwrap --unshare-net is Linux")
|
|
138
|
+
def test_nonexistent_pytest_path_exits_nonzero():
|
|
139
|
+
out = _run(["--pytest", "tests/no_such_1225.py"], timeout=90)
|
|
140
|
+
assert out.returncode != 0, out.stderr
|
|
141
|
+
assert out.returncode == 2, out.stderr
|
|
142
|
+
assert "isolation-unproven:" in out.stderr
|
|
143
|
+
assert "pytest exited" in out.stderr
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@pytest.mark.skipif(sys.platform != "linux", reason="bwrap --unshare-net is Linux")
|
|
147
|
+
def test_import_error_only_file_is_not_a_vacuous_pass(tmp_path):
|
|
148
|
+
tests = tmp_path / "tests"
|
|
149
|
+
tests.mkdir()
|
|
150
|
+
(tests / "test_only_import.py").write_text(
|
|
151
|
+
"import does_not_exist_1225\n", encoding="utf-8")
|
|
152
|
+
sys.path.insert(0, str(PROBE.parent))
|
|
153
|
+
import egress_probe
|
|
154
|
+
code = egress_probe.run_pytest(tmp_path, ["tests/test_only_import.py"], set())
|
|
155
|
+
assert code == 2, code
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@pytest.mark.skipif(sys.platform != "linux", reason="bwrap --unshare-net is Linux")
|
|
159
|
+
def test_pin_pytest_failure_without_a_connect_is_judged_by_the_trace(tmp_path):
|
|
160
|
+
"""pin: pytest exit 1 with a clean trace stays a pass."""
|
|
161
|
+
tests = tmp_path / "tests"
|
|
162
|
+
tests.mkdir()
|
|
163
|
+
(tests / "test_fails.py").write_text(
|
|
164
|
+
"def test_x():\n assert False\n", encoding="utf-8")
|
|
165
|
+
sys.path.insert(0, str(PROBE.parent))
|
|
166
|
+
import egress_probe
|
|
167
|
+
code = egress_probe.run_pytest(tmp_path, ["tests/test_fails.py"], set())
|
|
168
|
+
assert code == 0, code
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@pytest.mark.skipif(sys.platform != "linux", reason="bwrap --unshare-net is Linux")
|
|
172
|
+
def test_shim_through_pytest_exits_2(tmp_path, monkeypatch):
|
|
173
|
+
shim = tmp_path / "bwrap"
|
|
174
|
+
_shim(shim)
|
|
175
|
+
monkeypatch.setenv("EGRESS_PROBE_BWRAP", str(shim))
|
|
176
|
+
tree = tmp_path / "tree"
|
|
177
|
+
tests = tree / "tests"
|
|
178
|
+
tests.mkdir(parents=True)
|
|
179
|
+
(tests / "test_clean.py").write_text(
|
|
180
|
+
"def test_ok():\n assert True\n", encoding="utf-8")
|
|
181
|
+
sys.path.insert(0, str(PROBE.parent))
|
|
182
|
+
import egress_probe
|
|
183
|
+
import io
|
|
184
|
+
from contextlib import redirect_stderr
|
|
185
|
+
buf = io.StringIO()
|
|
186
|
+
with redirect_stderr(buf):
|
|
187
|
+
code = egress_probe.run_pytest(tree, ["tests/test_clean.py"], set())
|
|
188
|
+
err = buf.getvalue()
|
|
189
|
+
assert code == 2, err
|
|
190
|
+
assert "pytest isolation was not proven" in err
|
|
191
|
+
assert "ENETUNREACH" in err
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@pytest.mark.skipif(sys.platform != "linux", reason="bwrap --unshare-net is Linux")
|
|
195
|
+
def test_m4_removing_prove_isolation_is_killed(tmp_path):
|
|
196
|
+
source = PROBE.read_text(encoding="utf-8")
|
|
197
|
+
assert _ISOLATION_GUARD in source
|
|
198
|
+
mutated = source.replace(_ISOLATION_GUARD, " # M4 removed prove_isolation\n", 1)
|
|
199
|
+
repo = tmp_path / "repo"
|
|
200
|
+
(repo / "tests").mkdir(parents=True)
|
|
201
|
+
(repo / "tests" / "egress_probe.py").write_text(mutated, encoding="utf-8")
|
|
202
|
+
(repo / "tests" / "test_1219_egress_probe.py").write_text(
|
|
203
|
+
(ROOT / "tests" / "test_1219_egress_probe.py").read_text(encoding="utf-8"),
|
|
204
|
+
encoding="utf-8")
|
|
205
|
+
(repo / "EGRESS.md").write_text("# none\n", encoding="utf-8")
|
|
206
|
+
out = subprocess.run(
|
|
207
|
+
[sys.executable, "-m", "pytest", "-q", "--tb=line",
|
|
208
|
+
"tests/test_1219_egress_probe.py::test_shim_through_pytest_exits_2"],
|
|
209
|
+
cwd=repo, capture_output=True, text=True, timeout=90)
|
|
210
|
+
assert out.returncode != 0, out.stdout + out.stderr
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _mini_repo(tmp_path: Path) -> Path:
|
|
214
|
+
repo = tmp_path / "repo"
|
|
215
|
+
src = repo / "src" / "swarph_cli"
|
|
216
|
+
src.mkdir(parents=True)
|
|
217
|
+
(src / "__init__.py").write_text("", encoding="utf-8")
|
|
218
|
+
# Fixed mutant, not external input. test_zz_leak.py is outside the
|
|
219
|
+
# two-file list make used at 62fb0ee.
|
|
220
|
+
(src / "stall_alert.py").write_text(
|
|
221
|
+
"import os\n"
|
|
222
|
+
"def leak():\n"
|
|
223
|
+
" os.system('curl --max-time 1 -s -o /dev/null "
|
|
224
|
+
"http://203.0.113.50/')\n",
|
|
225
|
+
encoding="utf-8")
|
|
226
|
+
tests = repo / "tests"
|
|
227
|
+
tests.mkdir()
|
|
228
|
+
(tests / "egress_probe.py").write_text(PROBE.read_text(encoding="utf-8"), encoding="utf-8")
|
|
229
|
+
(tests / "test_zz_leak.py").write_text(
|
|
230
|
+
"import sys\n"
|
|
231
|
+
"from pathlib import Path\n"
|
|
232
|
+
"sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'src'))\n"
|
|
233
|
+
"from swarph_cli.stall_alert import leak\n"
|
|
234
|
+
"def test_leak():\n"
|
|
235
|
+
" leak()\n",
|
|
236
|
+
encoding="utf-8")
|
|
237
|
+
(repo / "Makefile").write_text((ROOT / "Makefile").read_text(encoding="utf-8"), encoding="utf-8")
|
|
238
|
+
(repo / "EGRESS.md").write_text("# none\n", encoding="utf-8")
|
|
239
|
+
return repo
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@pytest.mark.skipif(sys.platform != "linux", reason="bwrap --unshare-net is Linux")
|
|
243
|
+
def test_plant_outside_the_old_list_fails_make(tmp_path):
|
|
244
|
+
repo = _mini_repo(tmp_path)
|
|
245
|
+
out = subprocess.run(
|
|
246
|
+
["make", "egress-probe"], cwd=repo, capture_output=True, text=True, timeout=90)
|
|
247
|
+
assert out.returncode != 0, out.stdout + out.stderr
|
|
248
|
+
assert "egress-probe-status 1" in out.stdout, out.stdout + out.stderr
|
|
249
|
+
assert "unlisted-connect" in out.stderr
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@pytest.mark.skipif(sys.platform != "linux", reason="bwrap --unshare-net is Linux")
|
|
253
|
+
def test_make_prints_unproven_status(tmp_path):
|
|
254
|
+
repo = _mini_repo(tmp_path)
|
|
255
|
+
env = os.environ.copy()
|
|
256
|
+
env["EGRESS_PROBE_BWRAP"] = "/no/such/bwrap"
|
|
257
|
+
out = subprocess.run(
|
|
258
|
+
["make", "egress-probe"], cwd=repo, capture_output=True, text=True,
|
|
259
|
+
timeout=60, env=env)
|
|
260
|
+
assert "egress-probe-status 2" in out.stdout, out.stdout + out.stderr
|
|
261
|
+
assert "isolation-unproven:" in out.stderr
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""card #1221 — quota_sync reads a real Claude Code /usage display.
|
|
2
|
+
|
|
3
|
+
Source: Claude Code /usage, run by the commander in the lab-ovh Claude
|
|
4
|
+
Code session. local-command-stdout at 2026-10-19 22:42Z. The text below
|
|
5
|
+
is that display, verbatim.
|
|
6
|
+
|
|
7
|
+
Kept windows, in order: Current session (the 5h window) and Current week
|
|
8
|
+
(all models). Current week (Fable) is the same weekly reset for one model
|
|
9
|
+
and is not sent. On the previous head this display parses as None.
|
|
10
|
+
"""
|
|
11
|
+
import logging
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
|
|
14
|
+
from swarph_cli import quota_sync as qs
|
|
15
|
+
|
|
16
|
+
DOT = "\u00b7"
|
|
17
|
+
|
|
18
|
+
CAPTURED_AT = datetime(2026, 10, 19, 22, 42, tzinfo=timezone.utc)
|
|
19
|
+
AS_OF = CAPTURED_AT.isoformat()
|
|
20
|
+
SESSION_RESET = "2026-10-20T02:49:00+00:00"
|
|
21
|
+
WEEKLY_RESET = "2026-10-26T16:59:00+00:00"
|
|
22
|
+
|
|
23
|
+
USAGE = """You are currently using your subscription to power your Claude Code usage
|
|
24
|
+
|
|
25
|
+
Current session: 0% used · resets Oct 20, 2:49am (UTC)
|
|
26
|
+
Current week (all models): 1% used · resets Oct 26, 4:59pm (UTC)
|
|
27
|
+
Current week (Fable): 0% used · resets Oct 26, 4:59pm (UTC)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_usage_display_sends_the_session_and_the_weekly_window():
|
|
32
|
+
record = qs.parse_pane_capture(USAGE, AS_OF)
|
|
33
|
+
assert record is not None
|
|
34
|
+
assert record["windows"] == [
|
|
35
|
+
{"used_pct": 0.0, "resets_at": SESSION_RESET, "as_of": AS_OF},
|
|
36
|
+
{"used_pct": 1.0, "resets_at": WEEKLY_RESET, "as_of": AS_OF},
|
|
37
|
+
]
|
|
38
|
+
assert "Fable" not in str(record["windows"])
|
|
39
|
+
body = qs.decide_put(None, record, CAPTURED_AT)
|
|
40
|
+
assert body is not None
|
|
41
|
+
assert body["quota_windows"] == record["windows"]
|
|
42
|
+
assert body["quota_as_of"] == AS_OF
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _session(pct: int, when: str) -> str:
|
|
46
|
+
return f"Current session: {pct}% used {DOT} resets {when} (UTC)\n"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _weekly(pct: int, when: str) -> str:
|
|
50
|
+
return (
|
|
51
|
+
f"Current week (all models): {pct}% used {DOT} resets {when} (UTC)\n"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _lines(session_when: str, weekly_when: str) -> str:
|
|
56
|
+
return _session(0, session_when) + _weekly(1, weekly_when)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_hour_only_and_minute_resets_both_parse():
|
|
60
|
+
record = qs.parse_pane_capture(
|
|
61
|
+
_lines("Oct 20, 4:59pm", "Oct 26, 5pm"), AS_OF)
|
|
62
|
+
assert record is not None and len(record["windows"]) == 2
|
|
63
|
+
assert record["windows"][0]["resets_at"] == "2026-10-20T16:59:00+00:00"
|
|
64
|
+
assert record["windows"][1]["resets_at"] == "2026-10-26T17:00:00+00:00"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_session_reset_without_a_date_is_the_next_one_after_as_of():
|
|
68
|
+
record = qs.parse_pane_capture(_lines("2:49am", "Oct 26, 4:59pm"), AS_OF)
|
|
69
|
+
assert record is not None and len(record["windows"]) == 2
|
|
70
|
+
assert record["windows"][0]["resets_at"] == "2026-10-20T02:49:00+00:00"
|
|
71
|
+
later = "2026-10-20T03:00:00+00:00"
|
|
72
|
+
after = qs.parse_pane_capture(_lines("2:49am", "Oct 26, 4:59pm"), later)
|
|
73
|
+
assert after is not None
|
|
74
|
+
assert after["windows"][0]["resets_at"] == "2026-10-21T02:49:00+00:00"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_one_parsed_window_sends_no_quota_windows(caplog):
|
|
78
|
+
caplog.set_level(logging.WARNING, logger="swarph_cli.quota_sync")
|
|
79
|
+
record = qs.parse_pane_capture(_lines("Oct 20, 2:49am", "not-a-time"), AS_OF)
|
|
80
|
+
assert record is None or "windows" not in record
|
|
81
|
+
body = qs.decide_put(None, record, CAPTURED_AT)
|
|
82
|
+
assert body is None or "quota_windows" not in body
|
|
83
|
+
assert any(
|
|
84
|
+
"sending no quota_windows" in rec.message for rec in caplog.records)
|
|
85
|
+
scalar = qs.decide_put(
|
|
86
|
+
None,
|
|
87
|
+
{"used_pct": 40.0, "credits": None, "reset_at": None,
|
|
88
|
+
"blocked": False, "source": "pane", "as_of": AS_OF},
|
|
89
|
+
CAPTURED_AT)
|
|
90
|
+
assert scalar is not None
|
|
91
|
+
assert "quota_windows" not in scalar
|
|
92
|
+
assert scalar["quota_used_pct"] == 40.0
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_two_session_lines_and_no_weekly_send_no_quota_windows():
|
|
96
|
+
text = _session(0, "Oct 20, 2:49am") + _session(5, "Oct 20, 3:49am")
|
|
97
|
+
record = qs.parse_pane_capture(text, AS_OF)
|
|
98
|
+
assert record is None or "windows" not in record
|
|
99
|
+
body = qs.decide_put(None, record, CAPTURED_AT)
|
|
100
|
+
assert body is None or "quota_windows" not in body
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_newer_display_replaces_the_older_one_on_the_same_capture():
|
|
104
|
+
older = _session(10, "Oct 20, 1:00am") + _weekly(20, "Oct 26, 3:00pm")
|
|
105
|
+
newer = _session(0, "Oct 20, 2:49am") + _weekly(1, "Oct 26, 4:59pm")
|
|
106
|
+
record = qs.parse_pane_capture(older + newer, AS_OF)
|
|
107
|
+
assert record is not None and len(record["windows"]) == 2
|
|
108
|
+
assert record["windows"] == [
|
|
109
|
+
{"used_pct": 0.0, "resets_at": "2026-10-20T02:49:00+00:00", "as_of": AS_OF},
|
|
110
|
+
{"used_pct": 1.0, "resets_at": WEEKLY_RESET, "as_of": AS_OF},
|
|
111
|
+
]
|
|
112
|
+
body = qs.decide_put(None, record, CAPTURED_AT)
|
|
113
|
+
assert body is not None
|
|
114
|
+
assert body["quota_windows"] == record["windows"]
|