swarph-cli 0.45.1__tar.gz → 0.45.2__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 (319) hide show
  1. {swarph_cli-0.45.1/src/swarph_cli.egg-info → swarph_cli-0.45.2}/PKG-INFO +1 -1
  2. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/pyproject.toml +1 -1
  3. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/__init__.py +1 -1
  4. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/board.py +74 -3
  5. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/cell_selfcheck.py +4 -1
  6. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/codegraph.py +181 -6
  7. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/codegraph_hook.py +5 -2
  8. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/compress.py +4 -2
  9. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/daemon.py +1 -1
  10. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/hooks.py +201 -0
  11. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/init.py +14 -3
  12. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/install_wake_hook.py +39 -14
  13. swarph_cli-0.45.2/src/swarph_cli/commands/memory_emit_hook.py +147 -0
  14. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/mesh.py +102 -8
  15. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/monitor.py +410 -0
  16. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/onboard.py +44 -8
  17. swarph_cli-0.45.2/src/swarph_cli/commands/postcompact_hook_output.py +114 -0
  18. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/ratify.py +1 -1
  19. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/wake_hook_output.py +35 -4
  20. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/watchdog.py +12 -2
  21. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/gateway/server.py +15 -2
  22. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/guide/GUIDE.md +4 -1
  23. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/main.py +2 -0
  24. {swarph_cli-0.45.1 → swarph_cli-0.45.2/src/swarph_cli.egg-info}/PKG-INFO +1 -1
  25. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli.egg-info/SOURCES.txt +11 -0
  26. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_307_task4_ask_reply.py +129 -0
  27. swarph_cli-0.45.2/tests/test_532_accept_flag.py +103 -0
  28. swarph_cli-0.45.2/tests/test_546_default_gateway.py +164 -0
  29. swarph_cli-0.45.2/tests/test_552_codegraph_gateway.py +231 -0
  30. swarph_cli-0.45.2/tests/test_555_relay_row_shape.py +70 -0
  31. swarph_cli-0.45.2/tests/test_556_relay_freshness.py +90 -0
  32. swarph_cli-0.45.2/tests/test_heartbeat_check.py +220 -0
  33. swarph_cli-0.45.2/tests/test_hooks_verify.py +163 -0
  34. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_init_command.py +21 -0
  35. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_init_provider_menu.py +75 -0
  36. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_install_wake_hook.py +95 -8
  37. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_mesh_command.py +4 -2
  38. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_meta_edge_identity.py +28 -0
  39. swarph_cli-0.45.2/tests/test_obligation_close_is_surfaced.py +57 -0
  40. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_onboard_command.py +87 -0
  41. swarph_cli-0.45.2/tests/test_postcompact_recall.py +219 -0
  42. swarph_cli-0.45.2/tests/test_shipped_units_match_the_cli.py +368 -0
  43. swarph_cli-0.45.1/tests/test_shipped_units_match_the_cli.py +0 -124
  44. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/LICENSE +0 -0
  45. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/README.md +0 -0
  46. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/setup.cfg +0 -0
  47. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/__main__.py +0 -0
  48. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/bench/__init__.py +0 -0
  49. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/bench/backends.py +0 -0
  50. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/bench/pack.py +0 -0
  51. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/bench/prices.py +0 -0
  52. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/bench/quality.py +0 -0
  53. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/bench/refresh_prices.py +0 -0
  54. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/bench/runner.py +0 -0
  55. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/bench/validate.py +0 -0
  56. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/caller.py +0 -0
  57. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/capture/__init__.py +0 -0
  58. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/capture/harden.py +0 -0
  59. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/capture/lineage.py +0 -0
  60. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/capture/liveness.py +0 -0
  61. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/capture/manifest.py +0 -0
  62. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/capture/paths.py +0 -0
  63. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/capture/verify.py +0 -0
  64. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/cell.py +0 -0
  65. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/chain_token.py +0 -0
  66. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/__init__.py +0 -0
  67. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/_content.py +0 -0
  68. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/_display.py +0 -0
  69. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/_gateway_client.py +0 -0
  70. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/add.py +0 -0
  71. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/bench.py +0 -0
  72. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/board_merge_check.py +0 -0
  73. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/brain.py +0 -0
  74. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/brain_ask.py +0 -0
  75. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/cell.py +0 -0
  76. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/channel.py +0 -0
  77. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/chat.py +0 -0
  78. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/codex_hook_output.py +0 -0
  79. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/codex_waker.py +0 -0
  80. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/event.py +0 -0
  81. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/gateway.py +0 -0
  82. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/gh_route.py +0 -0
  83. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/group.py +0 -0
  84. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/guide.py +0 -0
  85. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/highlight.py +0 -0
  86. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/hook_output.py +0 -0
  87. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/import_session.py +0 -0
  88. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/install_codex_hooks.py +0 -0
  89. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/install_hook.py +0 -0
  90. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/install_multiplexer.py +0 -0
  91. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/lane.py +0 -0
  92. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/mcp_server.py +0 -0
  93. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/memory.py +0 -0
  94. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/memory_sync.py +0 -0
  95. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/okf_links.py +0 -0
  96. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/peer_reply_drain.py +0 -0
  97. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/protocol_handler.py +0 -0
  98. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/schedule.py +0 -0
  99. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/security.py +0 -0
  100. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/service.py +0 -0
  101. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/spawn.py +0 -0
  102. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/commands/timeline.py +0 -0
  103. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/compress/__init__.py +0 -0
  104. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/compress/levers.py +0 -0
  105. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/compress/marker.py +0 -0
  106. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/compress/verify.py +0 -0
  107. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/console_safe.py +0 -0
  108. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/delivery_queue.py +0 -0
  109. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/gateway/__init__.py +0 -0
  110. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/gateway/feature_registry.py +0 -0
  111. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/gateway/lanes_control.py +0 -0
  112. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/gateway/schema.sql +0 -0
  113. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/gateway/services_control.py +0 -0
  114. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/gh_identity.py +0 -0
  115. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/multiplexer.py +0 -0
  116. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/parsers/__init__.py +0 -0
  117. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/parsers/claude.py +0 -0
  118. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/peer_executor.py +0 -0
  119. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/peer_executor_windows.py +0 -0
  120. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/peer_reconciliation.py +0 -0
  121. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/peer_reply_outbox.py +0 -0
  122. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/peer_service_host.py +0 -0
  123. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/peer_service_payload.py +0 -0
  124. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/peer_service_reply.py +0 -0
  125. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/peer_staging.py +0 -0
  126. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/scripts/__init__.py +0 -0
  127. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/scripts/dm_notify_filter.py +0 -0
  128. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/scripts/ensure_monitor.sh +0 -0
  129. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/scripts/install_codex_waker_windows.ps1 +0 -0
  130. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/service/__init__.py +0 -0
  131. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/service/app.py +0 -0
  132. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/service/providers.py +0 -0
  133. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/session_bridge.py +0 -0
  134. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/stall_alert.py +0 -0
  135. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/systemd/codex-waker.default +0 -0
  136. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/systemd/peer-reply-drain.default +0 -0
  137. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/systemd/swarph-codex-outbox@.service +0 -0
  138. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/systemd/swarph-codex-outbox@.timer +0 -0
  139. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/systemd/swarph-codex-waker@.service +0 -0
  140. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/systemd/swarph-codex-waker@.timer +0 -0
  141. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/systemd/swarph-peer-reply-drain@.service +0 -0
  142. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/systemd/swarph-peer-reply-drain@.timer +0 -0
  143. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
  144. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
  145. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
  146. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli/tokens.py +0 -0
  147. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
  148. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli.egg-info/entry_points.txt +0 -0
  149. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli.egg-info/requires.txt +0 -0
  150. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/src/swarph_cli.egg-info/top_level.txt +0 -0
  151. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_201_win32_bash_refusal.py +0 -0
  152. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_243_onboard_per_peer_token.py +0 -0
  153. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_323_mistral_backend.py +0 -0
  154. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_397_gh_identity_router.py +0 -0
  155. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_397_router_preflight.py +0 -0
  156. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_467b_onboard_operator_credential.py +0 -0
  157. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_523_guide.py +0 -0
  158. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_524_conftest_hermeticity.py +0 -0
  159. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_84_cli_labels.py +0 -0
  160. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_add_security_gate.py +0 -0
  161. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_artifact_add.py +0 -0
  162. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_artifact_hash.py +0 -0
  163. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_artifact_lib.py +0 -0
  164. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_artifact_mcp.py +0 -0
  165. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_artifact_skill.py +0 -0
  166. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_artifact_tool.py +0 -0
  167. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_artifact_uri.py +0 -0
  168. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_bench_backends.py +0 -0
  169. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_bench_command.py +0 -0
  170. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_bench_pack.py +0 -0
  171. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_bench_prices.py +0 -0
  172. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_bench_quality.py +0 -0
  173. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_bench_runner.py +0 -0
  174. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_bench_validate.py +0 -0
  175. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_board_card_thread.py +0 -0
  176. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_board_command.py +0 -0
  177. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_board_merge_check.py +0 -0
  178. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_brain_ask_command.py +0 -0
  179. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_brain_ask_env_diagnosis.py +0 -0
  180. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_brain_ask_identity_explicitness.py +0 -0
  181. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_brain_command.py +0 -0
  182. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_caller_meta_guard.py +0 -0
  183. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_capture_lineage.py +0 -0
  184. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_capture_liveness.py +0 -0
  185. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_capture_manifest.py +0 -0
  186. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_capture_paths.py +0 -0
  187. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_capture_security.py +0 -0
  188. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_cell_command_dispatch.py +0 -0
  189. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_cell_harden.py +0 -0
  190. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_cell_loader.py +0 -0
  191. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_cell_selfcheck.py +0 -0
  192. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_cell_verify.py +0 -0
  193. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_chain_token.py +0 -0
  194. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_channel_command.py +0 -0
  195. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_channel_poll_never_breaks_dms.py +0 -0
  196. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_channel_post.py +0 -0
  197. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_channel_wake_policy_filter.py +0 -0
  198. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_chat_command.py +0 -0
  199. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_claude_parser.py +0 -0
  200. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_claude_tmux_template.py +0 -0
  201. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_codegraph_caller_scope.py +0 -0
  202. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_codegraph_command.py +0 -0
  203. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_codegraph_hook.py +0 -0
  204. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_codegraph_mcp.py +0 -0
  205. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_codex_waker.py +0 -0
  206. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_compress_caller_convention.py +0 -0
  207. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_compress_command.py +0 -0
  208. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_compress_levers.py +0 -0
  209. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_compress_marker.py +0 -0
  210. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_compress_verify.py +0 -0
  211. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_console_encoding_e2e.py +0 -0
  212. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_content_composition.py +0 -0
  213. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_cursor_membrane.py +0 -0
  214. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_daemon_command.py +0 -0
  215. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_daemon_console_encoding.py +0 -0
  216. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_daemon_delivery.py +0 -0
  217. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_delivery_queue.py +0 -0
  218. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_display_sanitize.py +0 -0
  219. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_ensure_monitor_no_peer_default.py +0 -0
  220. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_event_emit.py +0 -0
  221. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_feature_to_uri.py +0 -0
  222. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_gateway_brain.py +0 -0
  223. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_gateway_client.py +0 -0
  224. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_gateway_command.py +0 -0
  225. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_gateway_message_idempotency.py +0 -0
  226. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_gh_route_doctor_mesh_name.py +0 -0
  227. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_group_command.py +0 -0
  228. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_highlight_command.py +0 -0
  229. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_hook_output.py +0 -0
  230. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_hooks_activity_marker.py +0 -0
  231. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_hooks_add.py +0 -0
  232. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_hooks_bundle.py +0 -0
  233. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_hooks_init.py +0 -0
  234. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_hooks_legacy_generation_removal.py +0 -0
  235. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_hooks_lifecycle.py +0 -0
  236. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_hooks_merge.py +0 -0
  237. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_hooks_win32_bash_wrapper.py +0 -0
  238. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_hooks_win32_command_path.py +0 -0
  239. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_import_command.py +0 -0
  240. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_inbox_identity_provenance.py +0 -0
  241. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_install_codex_hooks.py +0 -0
  242. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_install_hook.py +0 -0
  243. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_install_multiplexer_command.py +0 -0
  244. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_lane_command.py +0 -0
  245. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_main.py +0 -0
  246. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_main_unknown_verb_guard.py +0 -0
  247. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_mcp_server.py +0 -0
  248. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_memory_command.py +0 -0
  249. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_memory_gateway.py +0 -0
  250. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_memory_sync.py +0 -0
  251. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_memory_sync_membrane.py +0 -0
  252. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_mesh_inbox_mark_read.py +0 -0
  253. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_mesh_inbox_sanitize.py +0 -0
  254. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_mesh_send_unknown_peer.py +0 -0
  255. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_mesh_sidecar.py +0 -0
  256. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_mesh_sidecar_cursor_decoupled.py +0 -0
  257. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_mesh_sidecar_wake_survives_read.py +0 -0
  258. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_monitor_channel_poll.py +0 -0
  259. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_monitor_cli.py +0 -0
  260. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_monitor_daemon_collision.py +0 -0
  261. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_monitor_self_name_verification.py +0 -0
  262. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_monitor_state_split.py +0 -0
  263. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_multiplexer.py +0 -0
  264. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_muse_membrane.py +0 -0
  265. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_muse_own_cli.py +0 -0
  266. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_okf_links.py +0 -0
  267. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_packaged_artifacts_exist.py +0 -0
  268. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_peer_executor.py +0 -0
  269. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_peer_executor_windows.py +0 -0
  270. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_peer_reply_drain_command.py +0 -0
  271. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_peer_reply_drain_supervisor.py +0 -0
  272. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_peer_reply_outbox.py +0 -0
  273. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_peer_reply_transport.py +0 -0
  274. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_peer_service_e2e.py +0 -0
  275. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_peer_service_host.py +0 -0
  276. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_peer_service_payload.py +0 -0
  277. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_peer_service_reply.py +0 -0
  278. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_process_liveness.py +0 -0
  279. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_protocol_handler.py +0 -0
  280. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_providers_isolation.py +0 -0
  281. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_ratify_command.py +0 -0
  282. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_repl_caller_convention.py +0 -0
  283. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_schedule_command.py +0 -0
  284. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_schedule_create_contract.py +0 -0
  285. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_security.py +0 -0
  286. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_service_app.py +0 -0
  287. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_service_command.py +0 -0
  288. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_service_providers.py +0 -0
  289. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_session_bridge_inject.py +0 -0
  290. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_session_bridge_probe.py +0 -0
  291. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_session_bridge_resolve.py +0 -0
  292. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_session_path_role_gate.py +0 -0
  293. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_smoke_chat.py +0 -0
  294. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_smoke_one_shot.py +0 -0
  295. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_smoke_phase_5_5.py +0 -0
  296. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_spawn_base_hoist.py +0 -0
  297. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_spawn_command.py +0 -0
  298. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_spawn_identity_stamp.py +0 -0
  299. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_spawn_live_pin.py +0 -0
  300. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_spawn_mitosis_lineage.py +0 -0
  301. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_spawn_tmux_session.py +0 -0
  302. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_spawn_windows_relaunch.py +0 -0
  303. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_stall_alert.py +0 -0
  304. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_subprocess_decode_encoding.py +0 -0
  305. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_timeline_command.py +0 -0
  306. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_token_file_one_parser.py +0 -0
  307. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_token_precedence_explicit_first.py +0 -0
  308. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_unified_token_resolver.py +0 -0
  309. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_version_consistency.py +0 -0
  310. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_vibe_membrane.py +0 -0
  311. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_watchdog.py +0 -0
  312. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_watchdog_dm_wake.py +0 -0
  313. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
  314. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_watchdog_dm_wake_wiring.py +0 -0
  315. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_watchdog_emit_health.py +0 -0
  316. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_watchdog_install_flags.py +0 -0
  317. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_watchdog_model_rung.py +0 -0
  318. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/tests/test_watchdog_stale_peers.py +0 -0
  319. {swarph_cli-0.45.1 → swarph_cli-0.45.2}/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.45.1
3
+ Version: 0.45.2
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.45.1"
7
+ version = "0.45.2"
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.45.1"
19
+ __version__ = "0.45.2"
20
20
 
21
21
  __all__ = ["__version__"]
@@ -12,12 +12,24 @@ from __future__ import annotations
12
12
 
13
13
  import argparse
14
14
  import json
15
+ import re
15
16
  import sys
16
17
  import urllib.error
17
18
  import urllib.parse
18
19
  import urllib.request
19
20
  from typing import Optional
20
21
 
22
+ # #532 — negative-branch marker detection. THREE COPIES exist and no single
23
+ # suite pins all three: here, mesh-gateway server.py (_accept_state), and
24
+ # mesh-gateway scripts/obligation_sweep.py. Unpinned-by-design is not the same
25
+ # decision as standalone-by-design — each repo's tests pin the same seven
26
+ # phrases. The detector is TWO-SIDED (drop-on-meta-edge, PR #124): it
27
+ # over-reads wishes ("must not fail") and under-reads real checks without the
28
+ # token ("Otherwise reject"); word boundaries remove the incidental compounds
29
+ # (failover, fail-safe, failsafe). Detection is named, never concluded — no
30
+ # substring test can know a falsifier exists.
31
+ _FAIL_MARKER_RE = re.compile(r"(?<![\w-])fail(?:s|ed|ing|ures?)?(?![\w-])", re.IGNORECASE)
32
+
21
33
  from swarph_cli.commands._content import ContentError, add_content_args, resolve_content
22
34
  from swarph_cli.commands._display import sanitize_terminal as _s
23
35
  from swarph_cli.commands.mesh import (
@@ -196,6 +208,41 @@ def _thread_recipient(card: dict, explicit_to: Optional[str]) -> str:
196
208
  )
197
209
 
198
210
 
211
+ def _say_line(resp: dict, card_id, to_node: str) -> str:
212
+ """The post confirmation — AND any obligation the post just DISCHARGED.
213
+
214
+ >>> THE GATEWAY ALREADY SAID SO AND THIS CLI THREW IT AWAY. <<< POST
215
+ /messages returns `closed_obligations: [ids]` (server.py:4077-4099): any
216
+ message a HOLDER posts to a thread closes their oldest open obligation on
217
+ it, unconditionally, without consulting the `accept` falsifier #532 added.
218
+ The old formatter read `id` only, so the terminal printed "posted id=NNN"
219
+ and nothing else.
220
+
221
+ MEASURED CONSEQUENCE (science-claude, card #562): obligation #22 on card
222
+ #544 closed at 06:59:15 on a status post whose literal content was that the
223
+ work was NOT done — one membrane passing, four CANNOT_EVALUATE, two
224
+ proposals unstarted. IT STAYED WRONGLY CLOSED FOR SIX HOURS while its holder
225
+ said in five further messages that it was unmet, and nothing compared the
226
+ two. `obligation_sweep.py` could not catch it either: that sweep selects
227
+ `status = 'open'`, so a wrongly-closed row falls out of the set forever.
228
+
229
+ The signal existed, was correct, and travelled the wire. It died at the last
230
+ hop, in the formatter. A one-line print is not the fix for the auto-close
231
+ POLICY (that is #562's own question) -- it is the fix for the holder not
232
+ being told, which is what made six hours of divergence possible.
233
+ """
234
+ line = f"posted id={resp.get('id')} onto card #{card_id} (to {to_node})"
235
+ closed = resp.get("closed_obligations") or []
236
+ if closed:
237
+ ids = ", ".join(f"#{i}" for i in closed)
238
+ line += (f"\n >>> THIS POST CLOSED OBLIGATION {ids}. <<< Posting to a "
239
+ f"thread discharges your oldest open obligation on it — the "
240
+ f"accept check was NOT evaluated. If the work is not actually "
241
+ f"done, say so now: a closed obligation leaves the sweep set "
242
+ f"and nothing will chase it again.")
243
+ return line
244
+
245
+
199
246
  def _card_say_payload(from_node: str, to_node: str, kind: str, content: str,
200
247
  thread_uuid: str) -> dict:
201
248
  return {
@@ -215,13 +262,31 @@ def _format_ask(d) -> str:
215
262
  An obligation with no timeout never goes red on its own, and the operator has to
216
263
  be told that AT THE MOMENT THEY CREATE IT — afterwards it looks identical to one
217
264
  that simply is not late yet.
265
+
266
+ #532 applies the same rule to the falsifier: an obligation with no accept
267
+ check reads RED in the sweep, and one whose check has no FAIL branch is
268
+ marked NO-FAIL-BRANCH — both are said AT MINT TIME, because afterwards the
269
+ rows look identical to sound ones.
218
270
  """
219
271
  when = d.get("timeout_at")
220
272
  deadline = f"overdue after {when}" if when else (
221
273
  "NO TIMEOUT — this never goes red on its own; pass --timeout-hours if it should")
274
+ accept = d.get("accept")
275
+ if not accept:
276
+ falsifier = ("NO ACCEPT CHECK — reads RED in the sweep; pass --accept "
277
+ "\"PASS = ... | FAIL = ...\" naming an observable and a way "
278
+ "it comes out negative")
279
+ elif not _FAIL_MARKER_RE.search(str(accept)):
280
+ falsifier = ("accept check has NO FAIL BRANCH — marked NO-FAIL-BRANCH in "
281
+ "the sweep; a check that cannot come out negative is not one")
282
+ else:
283
+ falsifier = f"accept: {accept}"
222
284
  return (f"obligation #{d.get('id')} on card #{d.get('card_id')}: "
223
285
  f"{d.get('holder')} owes it, status={d.get('status')}, {deadline}\n"
224
- f" thread {d.get('thread_uuid')} — the holder's reply in this thread closes it")
286
+ f" {falsifier}\n"
287
+ f" thread {d.get('thread_uuid')} — closes on the holder's "
288
+ f"`mesh reply` IN that thread; a plain `mesh send` cannot close it "
289
+ f"(#509 — the VERB is the mechanism, the thread is where it lands)")
225
290
 
226
291
 
227
292
  def _format_thread(data) -> str:
@@ -301,6 +366,11 @@ def _build_parser() -> argparse.ArgumentParser:
301
366
  ck.add_argument("--timeout-hours", type=int, default=None,
302
367
  help="hours until this obligation reads OVERDUE (default: none, "
303
368
  "which means it never goes red on its own)")
369
+ ck.add_argument("--accept", default=None, metavar='"PASS = ... | FAIL = ..."',
370
+ help="the falsifiable check that closes this obligation "
371
+ "(#532). Omitting it mints an obligation that reads RED "
372
+ "in the sweep; a check with no FAIL branch is marked "
373
+ "NO-FAIL-BRANCH — 'verify it works' is not a check")
304
374
  ck.add_argument("--kind", default="action", help="obligation kind (default: action)")
305
375
  ck.add_argument("--json", action="store_true"); _add_common(ck)
306
376
 
@@ -426,6 +496,8 @@ def run_board(argv: list[str]) -> int:
426
496
  "kind": args.kind}
427
497
  if args.timeout_hours is not None:
428
498
  body["timeout_hours"] = args.timeout_hours
499
+ if args.accept is not None:
500
+ body["accept"] = args.accept
429
501
  st, d = _post_json(f"{gw}/board/cards/{args.id}/ask", body, token)
430
502
  return _out(st, d, _format_ask, args.json)
431
503
  if args.command == "say":
@@ -461,8 +533,7 @@ def run_board(argv: list[str]) -> int:
461
533
  # explicit `propose` grant. `_out` passes `detail` through whole; it does
462
534
  # not need help, and a second copy of that logic is a second thing to
463
535
  # keep in sync.
464
- return _out(st, d, lambda x: f"posted id={x.get('id')} onto card "
465
- f"#{args.id} (to {to_node})", aj)
536
+ return _out(st, d, lambda x: _say_line(x, args.id, to_node), aj)
466
537
  if args.command == "ready":
467
538
  st, d = _patch_json(f"{gw}/board/cards/{args.id}", {"actor": self_name, "move_ready": not args.clear}, token)
468
539
  return _out(st, d, lambda x: f"card #{x.get('id')} move_ready -> {x.get('move_ready')}", aj)
@@ -289,7 +289,10 @@ def unit_is_relevant(name: str, text: str) -> bool:
289
289
  # If the canonical chain grows a leg, this one must too — that is the known
290
290
  # cost of the circularity break, stated here rather than discovered.
291
291
 
292
- _RESOLVER_DEFAULT_GATEWAY = "http://localhost:8788"
292
+ # TAILNET IP, NOT localhost and NOT a MagicDNS name (commander, 2026-08-21).
293
+ # The mesh-gateway binds HOST=100.107.222.72 ONLY; localhost has never been bound.
294
+ # MESH_GATEWAY_URL overrides — the escape hatch for anyone outside this mesh.
295
+ _RESOLVER_DEFAULT_GATEWAY = os.environ.get("MESH_GATEWAY_URL", "http://100.107.222.72:8788")
293
296
 
294
297
 
295
298
  def resolver_gateway(env: dict) -> str:
@@ -32,6 +32,7 @@ import json
32
32
  import os
33
33
  import re
34
34
  import sqlite3
35
+ import sys
35
36
 
36
37
  DEFAULT_INDEX = os.path.expanduser("~/.swarph/codegraph/index.db")
37
38
  # NOTE: named DEFAULT_CALLER_CELL, not "*_CALLER" — this is an A8 caller-CELL
@@ -41,6 +42,13 @@ DEFAULT_INDEX = os.path.expanduser("~/.swarph/codegraph/index.db")
41
42
  # against that unrelated dotted-slug convention.
42
43
  DEFAULT_CALLER_CELL = "lab-ovh"
43
44
 
45
+ # NOT 1 and NOT 2. A distinct code so "nothing was searched" can never be confused
46
+ # with a crash or a usage error, and so a caller can branch on it specifically.
47
+ # This is the AVAILABILITY axis, the same thing the gateway answers with a 503 --
48
+ # separate from the VISIBILITY axis stdout carries. rc is the one channel the frozen
49
+ # stdout contract left free.
50
+ RC_NO_INDEX = 3
51
+
44
52
  # --- textsearch (ported verbatim from codegraph_index/textsearch.py) ------
45
53
 
46
54
  _STOP = {"the", "a", "an", "is", "are", "was", "how", "does", "do", "of", "to", "in", "on", "for", "and", "or",
@@ -212,24 +220,191 @@ def format_human(rows, term) -> str:
212
220
  lines.append(f"{i}. {r['name']} [{r['kind']}] {r['repo']} {loc}{callers}")
213
221
  if r["signature"]:
214
222
  lines.append(f" {r['signature']}")
215
- if r["docstring"]:
216
- first = r["docstring"].strip().splitlines()[0][:100] if r["docstring"].strip() else ""
223
+ # >>> .get, AND ONLY HERE. <<< The RELAYED rows (#552) carry seven keys --
224
+ # repo, name, kind, file_path, start_line, callers, signature -- and NOT
225
+ # docstring, which the endpoint does not project. Every other key is common
226
+ # to both producers, so they stay subscripted: a missing `name` or
227
+ # `file_path` is a BROKEN PAYLOAD and must raise rather than format as blank.
228
+ # Loosening all of them would turn a real fault into a quiet half-answer.
229
+ doc = (r.get("docstring") or "").strip()
230
+ if doc:
231
+ first = doc.splitlines()[0][:100]
217
232
  if first:
218
233
  lines.append(f" {first}")
219
234
  return "\n".join(lines)
220
235
 
221
236
 
237
+ def _resolve_gateway(arg) -> str:
238
+ """Gateway base URL for the SEARCH-RELAY path (#552).
239
+
240
+ >>> THE CANONICAL VARIABLE IS MESH_GATEWAY_URL, AND SWARPH_BRAIN_GATEWAY IS
241
+ DELIBERATELY *NOT* IN THIS CHAIN. <<<
242
+
243
+ The first draft copied `highlight`'s chain, which ends in SWARPH_BRAIN_GATEWAY.
244
+ drop-on-meta-edge measured what that actually resolves to on this box:
245
+
246
+ SWARPH_CODEGRAPH_GATEWAY <unset>
247
+ SWARPH_GATEWAY <unset>
248
+ SWARPH_BRAIN_GATEWAY http://100.107.222.72:8788 <- brain-NAMED, MESH value
249
+ MESH_GATEWAY_URL http://100.107.222.72:8788 <- correct, and NOT consulted
250
+
251
+ The relay worked ONLY because a brain-named variable was mis-set to the mesh
252
+ gateway's address. Every link that resolved was unset or wrong-by-name, and the
253
+ one that was right was the one not read -- while #546 canonicalised
254
+ MESH_GATEWAY_URL across four modules the same morning.
255
+
256
+ >>> AND THE FAILURE IS TRIGGERED BY SOMEONE DOING THE RIGHT THING. <<<
257
+ SWARPH_BRAIN_GATEWAY names the brain (:8792, mint :8793) -- a different service on
258
+ different ports. The day anyone points it where its NAME says, this POSTs to
259
+ :8792/codegraph: a mesh peer token handed to a service it was not minted for, a
260
+ non-2xx, the fallback, and droplet's original symptom returns ON THE DAY SOMEONE
261
+ FIXED A VARIABLE. A fallback to a differently-named service is not a safety net;
262
+ it re-arms the collision on the day it fires.
263
+
264
+ `--gateway` / `--local` override.
265
+ """
266
+ return (arg
267
+ or os.environ.get("SWARPH_CODEGRAPH_GATEWAY")
268
+ or os.environ.get("SWARPH_GATEWAY")
269
+ or os.environ.get("MESH_GATEWAY_URL")
270
+ or "").strip()
271
+
272
+
273
+ def _query_via_gateway(gateway: str, query: str, limit: int, token_file):
274
+ """POST to the gateway's /codegraph — THE SERVER SEARCHES, WE GET THE ANSWER.
275
+
276
+ >>> WHY A RELAY AND NOT A SHIPPED INDEX (commander, 2026-08-21): one index,
277
+ one truth. Copying the db to every cell makes N stores that drift, which is
278
+ the divergence this fleet hit four separate ways the same week. <<<
279
+
280
+ The endpoint takes NO caller field on purpose -- the caller comes from the
281
+ bearer token and cannot be self-asserted the way `--caller-cell` can here.
282
+ So the relayed path is strictly *less* spoofable than the local one.
283
+
284
+ Returns (rows, index_present, freshness).
285
+
286
+ index_present is False when the SERVER says 503 -- the availability axis, kept
287
+ distinct from an empty result.
288
+
289
+ >>> WE DELIBERATELY IGNORE THE BODY'S `index_present` FIELD. <<< (drop, review of
290
+ PR #280, measured against the live endpoint.) The endpoint hardcodes it True on
291
+ its only 200 path and raises 503 otherwise, so on the wire the field carries no
292
+ information. The 503 IS the signal. A later "improvement" that reads the body
293
+ field instead would be branching on a constant and would silently stop detecting
294
+ the very thing it appears to check.
295
+
296
+ `freshness` is a list of per-repo dicts -- repo, indexed_at, index_age_hours,
297
+ stale, basis. The first cut of this relay DROPPED it, which meant the automatic
298
+ grep hook warned about a stale index while the HAND-TYPED verb did not -- and the
299
+ hand-typed verb is the consumer this relay was built for, because that is how
300
+ droplet hit the original problem. Card #193 exists because this index has run
301
+ BACKWARD before, so a stale index answering confidently is an OBSERVED failure,
302
+ not a hypothetical.
303
+ """
304
+ from swarph_cli.commands.mesh import _post_json, _resolve_token # local import: keeps
305
+ # the local-only path free of any mesh dependency, so an offline cell still works.
306
+
307
+ self_name = os.environ.get("SWARPH_SELF") or DEFAULT_CALLER_CELL
308
+ token = _resolve_token(self_name, token_file)
309
+ body = {"query": query, "limit": max(1, min(int(limit), 25))}
310
+ status, payload = _post_json(gateway.rstrip("/") + "/codegraph", body, token)
311
+ if status == 503:
312
+ return [], False, []
313
+ if status >= 400:
314
+ raise RuntimeError(f"gateway returned {status}: {str(payload)[:160]}")
315
+ payload = payload or {}
316
+ return payload.get("results", []), True, (payload.get("freshness") or [])
317
+
318
+
222
319
  def run_codegraph(argv) -> int:
223
320
  ap = argparse.ArgumentParser(
224
321
  prog="swarph codegraph",
225
322
  description="On-demand structural code search: find where a symbol is defined or what calls it, "
226
- "across the indexed repos. Natural-language query.")
323
+ "across the indexed repos. Natural-language query. Queries the mesh gateway when one "
324
+ "is configured (the server searches and returns the answer); falls back to a local index.")
227
325
  ap.add_argument("query", help="natural-language query, e.g. 'which function escapes HTML'")
228
326
  ap.add_argument("--index", default=DEFAULT_INDEX, help=f"index db (default {DEFAULT_INDEX})")
229
327
  ap.add_argument("--caller-cell", default=DEFAULT_CALLER_CELL, help="A8 caller identity (default lab-ovh)")
230
328
  ap.add_argument("--limit", type=int, default=8)
231
329
  ap.add_argument("--json", action="store_true", help="machine-readable JSON output")
330
+ ap.add_argument("--gateway", default=None,
331
+ help="mesh-gateway base URL for relayed search (else $SWARPH_CODEGRAPH_GATEWAY / "
332
+ "$SWARPH_GATEWAY / $SWARPH_BRAIN_GATEWAY). The server holds the index.")
333
+ ap.add_argument("--local", action="store_true",
334
+ help="force the local index even when a gateway is configured")
335
+ ap.add_argument("--token-file", default=None, help="explicit bearer token file")
232
336
  a = ap.parse_args(argv)
233
- rows = structural_query(a.query, index_path=a.index, caller_cell=a.caller_cell, limit=a.limit)
234
- print(json.dumps(rows, indent=2) if a.json else format_human(rows, a.query))
235
- return 0
337
+
338
+ gateway = "" if a.local else _resolve_gateway(a.gateway)
339
+ source = "local"
340
+ # The LOCAL path computes no freshness -- the index file is right there and the
341
+ # hook that used to state its age reads the mtime itself. Only the relayed path
342
+ # receives it, and only the relayed path can be silently answering from a store
343
+ # the user cannot see.
344
+ freshness = []
345
+ if gateway:
346
+ try:
347
+ rows, index_present, freshness = _query_via_gateway(
348
+ gateway, a.query, a.limit, a.token_file)
349
+ source = "gateway"
350
+ except Exception as exc: # noqa: BLE001
351
+ # >>> LOUD, NEVER A SILENT FALL BACK TO LOCAL. <<< A relay failure that
352
+ # degrades quietly to an empty local index is indistinguishable from
353
+ # "no matches" -- the exact confusion this change exists to remove.
354
+ print(f"swarph codegraph: gateway {gateway} unreachable ({type(exc).__name__}: {exc}); "
355
+ f"falling back to the local index at {a.index}", file=sys.stderr)
356
+ rows = structural_query(a.query, index_path=a.index,
357
+ caller_cell=a.caller_cell, limit=a.limit)
358
+ index_present = os.path.exists(os.path.expanduser(a.index))
359
+ else:
360
+ rows = structural_query(a.query, index_path=a.index,
361
+ caller_cell=a.caller_cell, limit=a.limit)
362
+ index_present = os.path.exists(os.path.expanduser(a.index))
363
+
364
+ stale = [f for f in freshness if f.get("stale")]
365
+ if stale:
366
+ # >>> A STALE INDEX ANSWERING CONFIDENTLY IS THE #193 FAILURE, AND IT LOOKS
367
+ # EXACTLY LIKE A GOOD ANSWER. <<< stderr, not stdout: the rows are real and a
368
+ # machine consumer should still get them unmodified. The user needs to know
369
+ # the line numbers may have moved; the parser does not.
370
+ for f in stale:
371
+ print(f"swarph codegraph: STALE INDEX for {f.get('repo', '?')} — "
372
+ f"{f.get('index_age_hours', 0):.1f}h old ({f.get('basis', 'unknown basis')}); "
373
+ f"verify line numbers against the file before acting on them.",
374
+ file=sys.stderr)
375
+
376
+ if not index_present:
377
+ # >>> STDERR, AND --json STAYS A BARE LIST. <<< .claude/hooks/codegraph-on-grep.sh
378
+ # does `rows=json.loads(...)` and iterates, so wrapping stdout in an envelope
379
+ # would break the hook that fires on every grep in the fleet. The availability
380
+ # signal therefore goes to stderr, where it costs no consumer anything.
381
+ #
382
+ # >>> THE EXIT CODE IS THE CHANNEL THAT WAS FREE ALL ALONG (drop, review of
383
+ # PR #279). <<< The stdout contract is frozen -- codegraph-on-grep.sh does
384
+ # json.loads() and iterates, so an envelope breaks the caller that fires on
385
+ # every grep in the fleet. But rc costs that contract NOTHING: a consumer
386
+ # that ignores rc sees exactly what it saw before, and one that reads it gets
387
+ # the availability axis the gateway already expresses as a 503.
388
+ #
389
+ # The hook itself already branches on `[ $rc -ne 0 ]` and reports the query as
390
+ # UNCORROBORATED, so this lands in a handler that exists.
391
+ #
392
+ # It also closes the ambiguity my previous commit MOVED rather than removed:
393
+ # dropping the false "No structural matches" line left the human path at NO
394
+ # STDOUT, NO STDERR (the hook redirects it), EXIT 0 -- and silence plus success
395
+ # reads as "ran fine, nothing found". Same rc closes both halves.
396
+ print(f"swarph codegraph: NO INDEX AVAILABLE (source={source}) — this is NOT a "
397
+ f"negative result; nothing was searched.", file=sys.stderr)
398
+
399
+ if a.json:
400
+ print(json.dumps(rows, indent=2))
401
+ elif not index_present:
402
+ # >>> A WARNING IS NOT ENOUGH IF THE CLAIM IS STILL PRINTED UNDER IT. <<<
403
+ # `format_human` says "No structural matches for X" -- a claim about the CODE,
404
+ # made when nothing was searched. The human path owns both streams, so it does
405
+ # not make the claim; a warning above a contradicting result just asks the
406
+ # reader to pick, and they pick the one that looks like a result.
407
+ pass
408
+ else:
409
+ print(format_human(rows, a.query))
410
+ return 0 if index_present else RC_NO_INDEX
@@ -37,7 +37,7 @@ import urllib.request
37
37
  from pathlib import Path
38
38
  from typing import Optional
39
39
 
40
- DEFAULT_GATEWAY = os.environ.get("MESH_GATEWAY_URL", "http://localhost:8788")
40
+ DEFAULT_GATEWAY = os.environ.get("MESH_GATEWAY_URL", "http://100.107.222.72:8788")
41
41
  TIMEOUT_S = 6
42
42
  MAX_ROWS = 6
43
43
 
@@ -252,7 +252,10 @@ def run_codegraph_hook(argv: Optional[list] = None) -> int:
252
252
  return 0
253
253
  tp = _token_path(self_name)
254
254
  try:
255
- token = tp.read_text().strip()
255
+ # encoding="utf-8": on Windows a bare read_text() uses the locale codec, and a
256
+ # non-ASCII byte in the token file crashes the hook with a charmap traceback
257
+ # that names a credential path.
258
+ token = tp.read_text(encoding="utf-8").strip()
256
259
  except OSError as e:
257
260
  _emit(f"CODEGRAPH UNAVAILABLE for '{term}' — peer token unreadable at {tp}: "
258
261
  f"{e}. Not a negative result: the graph was never asked.")
@@ -31,7 +31,9 @@ def run_compress(argv: list[str]) -> int:
31
31
  if not path.exists():
32
32
  print(f"[compress] no such file: {path}", file=sys.stderr)
33
33
  return 2
34
- source = path.read_text()
34
+ # utf-8 explicitly: a bare read_text() uses the locale codec, and on Windows
35
+ # (charmap) a non-ASCII byte in a marked file crashes the verb (#550).
36
+ source = path.read_text(encoding="utf-8")
35
37
  marker = parse_marker(source)
36
38
  if marker is None:
37
39
  print(f"[compress] REFUSE: {path} carries no swarph:compress marker — leaving untouched", file=sys.stderr)
@@ -47,7 +49,7 @@ def run_compress(argv: list[str]) -> int:
47
49
  print(f"[compress] archival: {path.name} {len(source)}B → {len(res.live)}B live "
48
50
  f"(+{len(res.archive)}B → {archive_path.name}); saved {saved}B always-loaded")
49
51
  if args.apply:
50
- archive_path.write_text(res.archive)
52
+ archive_path.write_text(res.archive, encoding="utf-8")
51
53
  _atomic_write(path, res.live)
52
54
  print(f"[compress] applied (.bak left, archive {archive_path.name})")
53
55
  return 0
@@ -85,7 +85,7 @@ def _build_parser() -> argparse.ArgumentParser:
85
85
  )
86
86
  p.add_argument(
87
87
  "--gateway",
88
- default=os.environ.get("MESH_GATEWAY_URL", "http://localhost:8788"),
88
+ default=os.environ.get("MESH_GATEWAY_URL", "http://100.107.222.72:8788"),
89
89
  help="mesh-gateway base URL.",
90
90
  )
91
91
  p.add_argument(