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.
Files changed (524) hide show
  1. {swarph_cli-0.72.25/src/swarph_cli.egg-info → swarph_cli-0.72.26}/PKG-INFO +1 -1
  2. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/pyproject.toml +1 -1
  3. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/__init__.py +1 -1
  4. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/mesh.py +11 -0
  5. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/quota_sync.py +133 -13
  6. {swarph_cli-0.72.25 → swarph_cli-0.72.26/src/swarph_cli.egg-info}/PKG-INFO +1 -1
  7. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli.egg-info/SOURCES.txt +1 -0
  8. swarph_cli-0.72.26/tests/test_1219_egress_probe.py +261 -0
  9. swarph_cli-0.72.26/tests/test_1221_quota_windows.py +114 -0
  10. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_mesh_command.py +40 -0
  11. swarph_cli-0.72.25/tests/test_1219_egress_probe.py +0 -109
  12. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/LICENSE +0 -0
  13. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/README.md +0 -0
  14. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/setup.cfg +0 -0
  15. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/__main__.py +0 -0
  16. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/bench/__init__.py +0 -0
  17. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/bench/backends.py +0 -0
  18. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/bench/claude_cli.py +0 -0
  19. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/bench/codex_cli.py +0 -0
  20. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/bench/pack.py +0 -0
  21. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/bench/prices.py +0 -0
  22. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/bench/providers.py +0 -0
  23. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/bench/quality.py +0 -0
  24. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/bench/refresh_prices.py +0 -0
  25. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/bench/rules.py +0 -0
  26. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/bench/runner.py +0 -0
  27. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/bench/signal_score.py +0 -0
  28. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/bench/validate.py +0 -0
  29. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/caller.py +0 -0
  30. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/capture/__init__.py +0 -0
  31. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/capture/harden.py +0 -0
  32. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/capture/lineage.py +0 -0
  33. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/capture/liveness.py +0 -0
  34. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/capture/manifest.py +0 -0
  35. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/capture/paths.py +0 -0
  36. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/capture/verify.py +0 -0
  37. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/cell.py +0 -0
  38. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/chain_token.py +0 -0
  39. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/channel.py +0 -0
  40. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/__init__.py +0 -0
  41. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/_content.py +0 -0
  42. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/_display.py +0 -0
  43. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/_gateway_client.py +0 -0
  44. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/add.py +0 -0
  45. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/bench.py +0 -0
  46. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/board.py +0 -0
  47. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/board_merge_check.py +0 -0
  48. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/brain.py +0 -0
  49. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/brain_ask.py +0 -0
  50. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/cell.py +0 -0
  51. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/cell_probe.py +0 -0
  52. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/cell_selfcheck.py +0 -0
  53. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/channel.py +0 -0
  54. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/chat.py +0 -0
  55. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/codegraph.py +0 -0
  56. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/codegraph_hook.py +0 -0
  57. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/codex_hook_output.py +0 -0
  58. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/codex_waker.py +0 -0
  59. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/compress.py +0 -0
  60. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/daemon.py +0 -0
  61. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/dreaming.py +0 -0
  62. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/event.py +0 -0
  63. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/gateway.py +0 -0
  64. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/gh_route.py +0 -0
  65. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/group.py +0 -0
  66. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/guide.py +0 -0
  67. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/highlight.py +0 -0
  68. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/hook_interpreter.py +0 -0
  69. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/hook_output.py +0 -0
  70. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/hooks.py +0 -0
  71. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/import_session.py +0 -0
  72. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/init.py +0 -0
  73. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/install_codex_hooks.py +0 -0
  74. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/install_hook.py +0 -0
  75. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/install_multiplexer.py +0 -0
  76. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/install_opencode_plugin.py +0 -0
  77. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/install_postcompact_hook.py +0 -0
  78. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/install_wake_hook.py +0 -0
  79. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/lane.py +0 -0
  80. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/mcp_server.py +0 -0
  81. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/memory.py +0 -0
  82. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/memory_emit_hook.py +0 -0
  83. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/memory_sync.py +0 -0
  84. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/monitor.py +0 -0
  85. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/okf_links.py +0 -0
  86. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/onboard.py +0 -0
  87. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/peer_reply_drain.py +0 -0
  88. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/postcompact_hook_output.py +0 -0
  89. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/protocol_handler.py +0 -0
  90. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/ratify.py +0 -0
  91. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/roster.py +0 -0
  92. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/rowclear_hook.py +0 -0
  93. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/schedule.py +0 -0
  94. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/security.py +0 -0
  95. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/service.py +0 -0
  96. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/spawn.py +0 -0
  97. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/sweep.py +0 -0
  98. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/timeline.py +0 -0
  99. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/triage.py +0 -0
  100. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/version.py +0 -0
  101. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/wake_hook_output.py +0 -0
  102. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/commands/watchdog.py +0 -0
  103. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/compress/__init__.py +0 -0
  104. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/compress/levers.py +0 -0
  105. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/compress/marker.py +0 -0
  106. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/compress/verify.py +0 -0
  107. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/console_safe.py +0 -0
  108. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/delivery_queue.py +0 -0
  109. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/dm_frame.py +0 -0
  110. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/dreaming/__init__.py +0 -0
  111. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/dreaming/candidates.py +0 -0
  112. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/dreaming/clone.py +0 -0
  113. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/dreaming/enrich.py +0 -0
  114. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/dreaming/index_check.py +0 -0
  115. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/dreaming/notify.py +0 -0
  116. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/dreaming/organize.py +0 -0
  117. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/dreaming/probes.py +0 -0
  118. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/dreaming/report.py +0 -0
  119. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/dreaming/run.py +0 -0
  120. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/dreaming/slm.py +0 -0
  121. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/dreaming/transcripts.py +0 -0
  122. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/dreaming/verify.py +0 -0
  123. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/gateway/__init__.py +0 -0
  124. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/gateway/feature_registry.py +0 -0
  125. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/gateway/lanes_control.py +0 -0
  126. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/gateway/schema.sql +0 -0
  127. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/gateway/server.py +0 -0
  128. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/gateway/services_control.py +0 -0
  129. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/gateway_default.py +0 -0
  130. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/gh_identity.py +0 -0
  131. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/guide/GUIDE.md +0 -0
  132. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/identity.py +0 -0
  133. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/launchd/swarph-codex-outbox.plist +0 -0
  134. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/launchd/swarph-codex-outbox.timer.plist +0 -0
  135. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/launchd/swarph-monitor.plist +0 -0
  136. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/launchd/swarph-watchdog.plist +0 -0
  137. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/launchd/swarph-watchdog.timer.plist +0 -0
  138. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/launchd_render.py +0 -0
  139. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/main.py +0 -0
  140. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/multiplexer.py +0 -0
  141. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/operator/__init__.py +0 -0
  142. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/operator/__main__.py +0 -0
  143. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/operator/api.py +0 -0
  144. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/operator/cli.py +0 -0
  145. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/operator/config.py +0 -0
  146. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/operator/fmt.py +0 -0
  147. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/orphan_daemons.py +0 -0
  148. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/pane_probe.py +0 -0
  149. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/parsers/__init__.py +0 -0
  150. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/parsers/claude.py +0 -0
  151. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/payloads/opencode/plugin.js +0 -0
  152. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/payloads/postcompact/_shim.cmd +0 -0
  153. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/payloads/postcompact/postcompact-recall-grok.sh +0 -0
  154. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/payloads/postcompact/postcompact-recall.sh +0 -0
  155. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/payloads/postcompact/posttooluse-emit-antigravity.sh +0 -0
  156. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/payloads/postcompact/posttooluse-emit-claude.sh +0 -0
  157. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/payloads/postcompact/posttooluse-emit-grok.sh +0 -0
  158. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/payloads/postcompact/posttooluse-emit.sh +0 -0
  159. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/payloads/postcompact/posttooluse-recall.sh +0 -0
  160. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/payloads/postcompact/precompact-flag.sh +0 -0
  161. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/payloads/postcompact/preinvocation-recall-antigravity.sh +0 -0
  162. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/payloads/postcompact/sessionstart-recall.sh +0 -0
  163. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/peer_executor.py +0 -0
  164. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/peer_executor_windows.py +0 -0
  165. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/peer_reconciliation.py +0 -0
  166. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/peer_reply_outbox.py +0 -0
  167. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/peer_service_host.py +0 -0
  168. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/peer_service_payload.py +0 -0
  169. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/peer_service_reply.py +0 -0
  170. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/peer_staging.py +0 -0
  171. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/query_filters.py +0 -0
  172. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/rowclear.py +0 -0
  173. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/scripts/__init__.py +0 -0
  174. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/scripts/dm_notify_filter.py +0 -0
  175. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/scripts/ensure_monitor.sh +0 -0
  176. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/scripts/install_codex_waker_windows.ps1 +0 -0
  177. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/scripts/install_monitor_task_windows.ps1 +0 -0
  178. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/scripts/wake_watchdog.py +0 -0
  179. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/scrub.py +0 -0
  180. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/service/__init__.py +0 -0
  181. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/service/app.py +0 -0
  182. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/service/providers.py +0 -0
  183. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/session_bridge.py +0 -0
  184. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/skills/i-have-adhd/LICENSE +0 -0
  185. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/skills/i-have-adhd/SKILL.md +0 -0
  186. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/source_text.py +0 -0
  187. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/stall_alert.py +0 -0
  188. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/codex-waker.default +0 -0
  189. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/peer-reply-drain.default +0 -0
  190. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-codex-outbox@.service +0 -0
  191. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-codex-outbox@.timer +0 -0
  192. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-codex-waker@.service +0 -0
  193. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-codex-waker@.timer +0 -0
  194. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-monitor-reexec.path +0 -0
  195. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-monitor-reexec.service +0 -0
  196. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-monitor@.service +0 -0
  197. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-peer-reply-drain@.service +0 -0
  198. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-peer-reply-drain@.timer +0 -0
  199. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-stuck-sweep.service +0 -0
  200. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-stuck-sweep.timer +0 -0
  201. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-triage.service +0 -0
  202. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-triage.timer +0 -0
  203. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
  204. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
  205. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
  206. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/timeline_paths.py +0 -0
  207. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/tokens.py +0 -0
  208. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/triage/__init__.py +0 -0
  209. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/triage/backend.py +0 -0
  210. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/triage/job.py +0 -0
  211. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/triage/owners.json +0 -0
  212. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/triage/router.py +0 -0
  213. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/triage/rows.py +0 -0
  214. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli/triage/sensor.py +0 -0
  215. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
  216. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli.egg-info/entry_points.txt +0 -0
  217. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli.egg-info/requires.txt +0 -0
  218. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/src/swarph_cli.egg-info/top_level.txt +0 -0
  219. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1006_board_publisher.py +0 -0
  220. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1006_session_board.py +0 -0
  221. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1010_strand_only.py +0 -0
  222. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1013_spawn_channel.py +0 -0
  223. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1017_muse_tui.py +0 -0
  224. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1050_surrogate_wake.py +0 -0
  225. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1052_opencode_deaf.py +0 -0
  226. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1059_watchdog_unread_env.py +0 -0
  227. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1061_roster_cli.py +0 -0
  228. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1064_stuck_sweep.py +0 -0
  229. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1065_release.py +0 -0
  230. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1066_quiet_wakes.py +0 -0
  231. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1068_structured_accept_cli.py +0 -0
  232. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1071_quota_sync.py +0 -0
  233. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1073_triage.py +0 -0
  234. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1073_triage_board.py +0 -0
  235. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1073_wheel_install.py +0 -0
  236. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1075_task_cli.py +0 -0
  237. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1083_openai_decisions.py +0 -0
  238. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1144_cell_yaml_github.py +0 -0
  239. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1197_commander_taps.py +0 -0
  240. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1203_swarph_me_refusal.py +0 -0
  241. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1206_offer_question.py +0 -0
  242. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_1214_egress.py +0 -0
  243. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_124_register_merge.py +0 -0
  244. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_126_pending_from_complete.py +0 -0
  245. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_132_ensure_monitor_state_dir.py +0 -0
  246. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_138_memory_navigate.py +0 -0
  247. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_139_tool_probe.py +0 -0
  248. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_183_card_anchor.py +0 -0
  249. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_184_notify_honesty.py +0 -0
  250. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_201_win32_bash_refusal.py +0 -0
  251. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_243_onboard_per_peer_token.py +0 -0
  252. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_307_task4_ask_reply.py +0 -0
  253. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_312_codegraph_fuzzy_verb.py +0 -0
  254. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_319_refusal_exit_codes.py +0 -0
  255. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_323_mistral_backend.py +0 -0
  256. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_332_report_organize_findings.py +0 -0
  257. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_356_query_filters.py +0 -0
  258. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_397_gh_identity_router.py +0 -0
  259. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_397_router_preflight.py +0 -0
  260. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_402_one_identity_resolver.py +0 -0
  261. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_432_rowclear.py +0 -0
  262. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_467b_onboard_operator_credential.py +0 -0
  263. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_472_register_caller_binding.py +0 -0
  264. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_484_codex_queue_sink.py +0 -0
  265. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_523_guide.py +0 -0
  266. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_524_conftest_hermeticity.py +0 -0
  267. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_532_accept_flag.py +0 -0
  268. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_535_version_capability.py +0 -0
  269. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_538_signal_bench.py +0 -0
  270. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_546_default_gateway.py +0 -0
  271. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_547_verbs_from_registry.py +0 -0
  272. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_548_gateway_remainder.py +0 -0
  273. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_552_codegraph_gateway.py +0 -0
  274. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_555_relay_row_shape.py +0 -0
  275. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_556_relay_freshness.py +0 -0
  276. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_559_edges_truncated.py +0 -0
  277. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_564_bootstrap_first_peer.py +0 -0
  278. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_578_no_machine_specific_host_defaults.py +0 -0
  279. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_579_register_bootstrap_message.py +0 -0
  280. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_591_cli_step_graph.py +0 -0
  281. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_644_monitor_supervision.py +0 -0
  282. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_648_install_unit.py +0 -0
  283. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_654_reexec.py +0 -0
  284. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_656_owned_slm.py +0 -0
  285. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_684_dreaming_verb.py +0 -0
  286. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_689_pkg_version_order.py +0 -0
  287. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_715_grok_wake.py +0 -0
  288. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_716_timeline_one_path.py +0 -0
  289. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_723_grok_0251.py +0 -0
  290. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_724_ps1_ascii_or_bom.py +0 -0
  291. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_725_stdio_reconfigure.py +0 -0
  292. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_726_grok_limit_screen.py +0 -0
  293. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_729_arm_check_delivery_gap.py +0 -0
  294. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_729_dm_once.py +0 -0
  295. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_729_wake_watchdog.py +0 -0
  296. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_734_dreaming_enrich_degrade.py +0 -0
  297. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_735_dm_mcp_tools.py +0 -0
  298. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_739_wake_writer.py +0 -0
  299. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_740_edit_project.py +0 -0
  300. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_762_cards_edit_priority.py +0 -0
  301. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_764_enrich_collapse.py +0 -0
  302. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_801_confirm_flow_cli.py +0 -0
  303. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_802_stage_history.py +0 -0
  304. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_825_codegraph_hook_noise.py +0 -0
  305. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_825_install_posttooluse_only.py +0 -0
  306. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_825_remaining_cap_shred_bind.py +0 -0
  307. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_829_counterfactual_column.py +0 -0
  308. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_830_hook_binding_version.py +0 -0
  309. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_84_cli_labels.py +0 -0
  310. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_866_doctrine_guide.py +0 -0
  311. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_872_identity_refuses_not_warns.py +0 -0
  312. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_874_source_text.py +0 -0
  313. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_888_opencode_data_dir_can_fail.py +0 -0
  314. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_937_dreaming_notify.py +0 -0
  315. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_938_organize_line_limit.py +0 -0
  316. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_941_bench_backends.py +0 -0
  317. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_941_harness_342.py +0 -0
  318. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_945_same_line_pairing.py +0 -0
  319. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_955_prompt_audit.py +0 -0
  320. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_957_auth_http.py +0 -0
  321. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_959_venv_interpreter.py +0 -0
  322. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_960_channel.py +0 -0
  323. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_960_ensure_monitor_heartbeat.py +0 -0
  324. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_961_cursor_grok_footer.py +0 -0
  325. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_961_loud_stop.py +0 -0
  326. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_963_plugin_manifest.py +0 -0
  327. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_964_channel_cell.py +0 -0
  328. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_965_mesh_wait.py +0 -0
  329. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_975_channel_hop.py +0 -0
  330. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_add_security_gate.py +0 -0
  331. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_artifact_add.py +0 -0
  332. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_artifact_hash.py +0 -0
  333. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_artifact_lib.py +0 -0
  334. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_artifact_mcp.py +0 -0
  335. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_artifact_skill.py +0 -0
  336. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_artifact_tool.py +0 -0
  337. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_artifact_uri.py +0 -0
  338. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_bench_backends.py +0 -0
  339. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_bench_claude_cli.py +0 -0
  340. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_bench_codex_cli.py +0 -0
  341. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_bench_command.py +0 -0
  342. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_bench_pack.py +0 -0
  343. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_bench_prices.py +0 -0
  344. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_bench_quality.py +0 -0
  345. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_bench_runner.py +0 -0
  346. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_bench_validate.py +0 -0
  347. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_board_card_thread.py +0 -0
  348. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_board_command.py +0 -0
  349. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_board_merge_check.py +0 -0
  350. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_board_obligation_close.py +0 -0
  351. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_brain_ask_command.py +0 -0
  352. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_brain_ask_env_diagnosis.py +0 -0
  353. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_brain_ask_identity_explicitness.py +0 -0
  354. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_brain_command.py +0 -0
  355. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_caller_meta_guard.py +0 -0
  356. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_capture_lineage.py +0 -0
  357. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_capture_liveness.py +0 -0
  358. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_capture_manifest.py +0 -0
  359. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_capture_paths.py +0 -0
  360. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_capture_security.py +0 -0
  361. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_cell_command_dispatch.py +0 -0
  362. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_cell_harden.py +0 -0
  363. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_cell_loader.py +0 -0
  364. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_cell_selfcheck.py +0 -0
  365. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_cell_verify.py +0 -0
  366. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_chain_token.py +0 -0
  367. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_channel_command.py +0 -0
  368. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_channel_poll_never_breaks_dms.py +0 -0
  369. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_channel_post.py +0 -0
  370. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_channel_wake_policy_filter.py +0 -0
  371. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_chat_command.py +0 -0
  372. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_claude_parser.py +0 -0
  373. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_claude_tmux_template.py +0 -0
  374. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_codegraph_caller_scope.py +0 -0
  375. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_codegraph_command.py +0 -0
  376. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_codegraph_hook.py +0 -0
  377. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_codegraph_mcp.py +0 -0
  378. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_codex_waker.py +0 -0
  379. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_compress_caller_convention.py +0 -0
  380. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_compress_command.py +0 -0
  381. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_compress_levers.py +0 -0
  382. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_compress_marker.py +0 -0
  383. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_compress_verify.py +0 -0
  384. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_console_encoding_e2e.py +0 -0
  385. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_content_composition.py +0 -0
  386. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_copilot_membrane.py +0 -0
  387. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_copilot_pane_reader.py +0 -0
  388. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_cursor_membrane.py +0 -0
  389. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_daemon_command.py +0 -0
  390. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_daemon_console_encoding.py +0 -0
  391. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_daemon_delivery.py +0 -0
  392. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_delivery_queue.py +0 -0
  393. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_display_sanitize.py +0 -0
  394. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_ensure_monitor_no_peer_default.py +0 -0
  395. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_event_emit.py +0 -0
  396. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_feature_to_uri.py +0 -0
  397. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_gateway_brain.py +0 -0
  398. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_gateway_client.py +0 -0
  399. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_gateway_command.py +0 -0
  400. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_gateway_message_idempotency.py +0 -0
  401. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_gh_route_doctor_mesh_name.py +0 -0
  402. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_group_command.py +0 -0
  403. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_guide_commands_parse.py +0 -0
  404. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_guide_selfcheck.py +0 -0
  405. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_heartbeat_check.py +0 -0
  406. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_highlight_command.py +0 -0
  407. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_hook_output.py +0 -0
  408. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_hooks_activity_marker.py +0 -0
  409. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_hooks_add.py +0 -0
  410. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_hooks_bundle.py +0 -0
  411. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_hooks_init.py +0 -0
  412. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_hooks_legacy_generation_removal.py +0 -0
  413. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_hooks_lifecycle.py +0 -0
  414. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_hooks_merge.py +0 -0
  415. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_hooks_verify.py +0 -0
  416. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_hooks_win32_bash_wrapper.py +0 -0
  417. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_hooks_win32_command_path.py +0 -0
  418. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_import_command.py +0 -0
  419. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_inbox_identity_provenance.py +0 -0
  420. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_init_command.py +0 -0
  421. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_init_provider_menu.py +0 -0
  422. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_install_codex_hooks.py +0 -0
  423. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_install_hook.py +0 -0
  424. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_install_multiplexer_command.py +0 -0
  425. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_install_postcompact_hook.py +0 -0
  426. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_install_wake_hook.py +0 -0
  427. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_lane_command.py +0 -0
  428. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_launchd_units.py +0 -0
  429. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_main.py +0 -0
  430. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_main_unknown_verb_guard.py +0 -0
  431. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_mcp_missing_hint.py +0 -0
  432. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_mcp_server.py +0 -0
  433. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_memory_command.py +0 -0
  434. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_memory_emit_snippet.py +0 -0
  435. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_memory_gateway.py +0 -0
  436. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_memory_sync.py +0 -0
  437. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_memory_sync_membrane.py +0 -0
  438. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_mesh_inbox_mark_read.py +0 -0
  439. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_mesh_inbox_sanitize.py +0 -0
  440. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_mesh_originate_thread_623.py +0 -0
  441. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_mesh_send_unknown_peer.py +0 -0
  442. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_mesh_sidecar.py +0 -0
  443. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_mesh_sidecar_cursor_decoupled.py +0 -0
  444. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_mesh_sidecar_muse_sink.py +0 -0
  445. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_mesh_sidecar_wake_survives_read.py +0 -0
  446. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_meta_edge_identity.py +0 -0
  447. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_monitor_channel_poll.py +0 -0
  448. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_monitor_cli.py +0 -0
  449. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_monitor_daemon_collision.py +0 -0
  450. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_monitor_self_name_verification.py +0 -0
  451. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_monitor_state_split.py +0 -0
  452. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_multiplexer.py +0 -0
  453. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_muse_membrane.py +0 -0
  454. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_muse_own_cli.py +0 -0
  455. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_obligation_close_is_surfaced.py +0 -0
  456. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_okf_links.py +0 -0
  457. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_onboard_command.py +0 -0
  458. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_opencode_membrane.py +0 -0
  459. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_opencode_plugin.py +0 -0
  460. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_operator_me.py +0 -0
  461. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_orphan_daemons.py +0 -0
  462. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_packaged_artifacts_exist.py +0 -0
  463. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_packaged_skills.py +0 -0
  464. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_peer_executor.py +0 -0
  465. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_peer_executor_windows.py +0 -0
  466. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_peer_reply_drain_command.py +0 -0
  467. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_peer_reply_drain_supervisor.py +0 -0
  468. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_peer_reply_outbox.py +0 -0
  469. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_peer_reply_transport.py +0 -0
  470. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_peer_service_e2e.py +0 -0
  471. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_peer_service_host.py +0 -0
  472. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_peer_service_payload.py +0 -0
  473. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_peer_service_reply.py +0 -0
  474. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_postcompact_recall.py +0 -0
  475. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_process_liveness.py +0 -0
  476. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_protocol_handler.py +0 -0
  477. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_providers_isolation.py +0 -0
  478. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_ratify_command.py +0 -0
  479. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_readme_claims_no_gateway_default.py +0 -0
  480. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_repl_caller_convention.py +0 -0
  481. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_schedule_command.py +0 -0
  482. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_schedule_create_contract.py +0 -0
  483. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_scrub.py +0 -0
  484. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_security.py +0 -0
  485. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_service_app.py +0 -0
  486. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_service_command.py +0 -0
  487. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_service_providers.py +0 -0
  488. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_session_bridge_inject.py +0 -0
  489. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_session_bridge_probe.py +0 -0
  490. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_session_bridge_resolve.py +0 -0
  491. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_session_path_role_gate.py +0 -0
  492. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_shipped_units_match_the_cli.py +0 -0
  493. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_smoke_chat.py +0 -0
  494. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_smoke_one_shot.py +0 -0
  495. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_smoke_phase_5_5.py +0 -0
  496. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_spawn_base_hoist.py +0 -0
  497. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_spawn_command.py +0 -0
  498. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_spawn_identity_stamp.py +0 -0
  499. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_spawn_live_pin.py +0 -0
  500. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_spawn_mitosis_lineage.py +0 -0
  501. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_spawn_tmux_session.py +0 -0
  502. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_spawn_windows_relaunch.py +0 -0
  503. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_stall_alert.py +0 -0
  504. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_subprocess_decode_encoding.py +0 -0
  505. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_timeline_command.py +0 -0
  506. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_tmux_wake_runstate_defer.py +0 -0
  507. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_tmux_wake_session_reanchor.py +0 -0
  508. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_tmux_wake_stale.py +0 -0
  509. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_tmux_wake_submit_verify.py +0 -0
  510. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_token_file_one_parser.py +0 -0
  511. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_token_precedence_explicit_first.py +0 -0
  512. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_unified_token_resolver.py +0 -0
  513. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_version_command.py +0 -0
  514. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_version_consistency.py +0 -0
  515. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_vibe_membrane.py +0 -0
  516. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_watchdog.py +0 -0
  517. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_watchdog_dm_wake.py +0 -0
  518. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
  519. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_watchdog_dm_wake_wiring.py +0 -0
  520. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_watchdog_emit_health.py +0 -0
  521. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_watchdog_install_flags.py +0 -0
  522. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_watchdog_model_rung.py +0 -0
  523. {swarph_cli-0.72.25 → swarph_cli-0.72.26}/tests/test_watchdog_stale_peers.py +0 -0
  524. {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.25
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.25"
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" }
@@ -16,6 +16,6 @@ The architecture splits CLI from substrate so:
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
- __version__ = "0.72.25"
19
+ __version__ = "0.72.26"
20
20
 
21
21
  __all__ = ["__version__"]
@@ -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 cells, agy labels, empty captures) is None.
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 {"used_pct": None, "credits": None, "reset_at": None,
55
- "blocked": True, "source": "error", "as_of": as_of}
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 {"used_pct": None, "credits": None, "reset_at": None,
58
- "blocked": True, "source": "pane", "as_of": as_of}
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 {"used_pct": 100, "credits": None, "reset_at": reset,
64
- "blocked": True, "source": "pane", "as_of": as_of}
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 {"used_pct": None, "credits": None, "reset_at": None,
69
- "blocked": False, "source": "pane", "as_of": as_of,
70
- "bound_left_pct": float(bound.group(1))}
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 {"used_pct": None, "credits": float(copilot.group(1)),
74
- "reset_at": None, "blocked": False, "source": "pane",
75
- "as_of": as_of}
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.25
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"]