swarph-cli 0.45.2__tar.gz → 0.46.0__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 (322) hide show
  1. {swarph_cli-0.45.2/src/swarph_cli.egg-info → swarph_cli-0.46.0}/PKG-INFO +2 -1
  2. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/README.md +1 -0
  3. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/pyproject.toml +1 -1
  4. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/__init__.py +1 -1
  5. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/board.py +52 -3
  6. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/gh_route.py +2 -0
  7. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/init.py +7 -1
  8. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/memory_emit_hook.py +15 -5
  9. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/mesh.py +169 -5
  10. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/monitor.py +1 -0
  11. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/onboard.py +1 -1
  12. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/spawn.py +63 -172
  13. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/wake_hook_output.py +3 -1
  14. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/guide/GUIDE.md +3 -2
  15. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/main.py +6 -1
  16. {swarph_cli-0.45.2 → swarph_cli-0.46.0/src/swarph_cli.egg-info}/PKG-INFO +2 -1
  17. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli.egg-info/SOURCES.txt +4 -0
  18. swarph_cli-0.46.0/tests/test_124_register_merge.py +112 -0
  19. swarph_cli-0.46.0/tests/test_319_refusal_exit_codes.py +147 -0
  20. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_532_accept_flag.py +6 -0
  21. swarph_cli-0.46.0/tests/test_535_version_capability.py +113 -0
  22. swarph_cli-0.46.0/tests/test_board_obligation_close.py +77 -0
  23. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_mesh_command.py +5 -1
  24. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_postcompact_recall.py +66 -0
  25. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_spawn_base_hoist.py +39 -0
  26. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_spawn_command.py +3 -1
  27. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/LICENSE +0 -0
  28. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/setup.cfg +0 -0
  29. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/__main__.py +0 -0
  30. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/bench/__init__.py +0 -0
  31. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/bench/backends.py +0 -0
  32. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/bench/pack.py +0 -0
  33. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/bench/prices.py +0 -0
  34. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/bench/quality.py +0 -0
  35. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/bench/refresh_prices.py +0 -0
  36. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/bench/runner.py +0 -0
  37. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/bench/validate.py +0 -0
  38. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/caller.py +0 -0
  39. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/capture/__init__.py +0 -0
  40. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/capture/harden.py +0 -0
  41. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/capture/lineage.py +0 -0
  42. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/capture/liveness.py +0 -0
  43. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/capture/manifest.py +0 -0
  44. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/capture/paths.py +0 -0
  45. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/capture/verify.py +0 -0
  46. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/cell.py +0 -0
  47. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/chain_token.py +0 -0
  48. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/__init__.py +0 -0
  49. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/_content.py +0 -0
  50. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/_display.py +0 -0
  51. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/_gateway_client.py +0 -0
  52. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/add.py +0 -0
  53. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/bench.py +0 -0
  54. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/board_merge_check.py +0 -0
  55. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/brain.py +0 -0
  56. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/brain_ask.py +0 -0
  57. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/cell.py +0 -0
  58. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/cell_selfcheck.py +0 -0
  59. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/channel.py +0 -0
  60. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/chat.py +0 -0
  61. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/codegraph.py +0 -0
  62. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/codegraph_hook.py +0 -0
  63. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/codex_hook_output.py +0 -0
  64. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/codex_waker.py +0 -0
  65. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/compress.py +0 -0
  66. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/daemon.py +0 -0
  67. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/event.py +0 -0
  68. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/gateway.py +0 -0
  69. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/group.py +0 -0
  70. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/guide.py +0 -0
  71. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/highlight.py +0 -0
  72. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/hook_output.py +0 -0
  73. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/hooks.py +0 -0
  74. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/import_session.py +0 -0
  75. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/install_codex_hooks.py +0 -0
  76. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/install_hook.py +0 -0
  77. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/install_multiplexer.py +0 -0
  78. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/install_wake_hook.py +0 -0
  79. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/lane.py +0 -0
  80. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/mcp_server.py +0 -0
  81. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/memory.py +0 -0
  82. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/memory_sync.py +0 -0
  83. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/okf_links.py +0 -0
  84. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/peer_reply_drain.py +0 -0
  85. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/postcompact_hook_output.py +0 -0
  86. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/protocol_handler.py +0 -0
  87. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/ratify.py +0 -0
  88. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/schedule.py +0 -0
  89. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/security.py +0 -0
  90. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/service.py +0 -0
  91. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/timeline.py +0 -0
  92. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/commands/watchdog.py +0 -0
  93. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/compress/__init__.py +0 -0
  94. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/compress/levers.py +0 -0
  95. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/compress/marker.py +0 -0
  96. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/compress/verify.py +0 -0
  97. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/console_safe.py +0 -0
  98. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/delivery_queue.py +0 -0
  99. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/gateway/__init__.py +0 -0
  100. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/gateway/feature_registry.py +0 -0
  101. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/gateway/lanes_control.py +0 -0
  102. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/gateway/schema.sql +0 -0
  103. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/gateway/server.py +0 -0
  104. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/gateway/services_control.py +0 -0
  105. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/gh_identity.py +0 -0
  106. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/multiplexer.py +0 -0
  107. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/parsers/__init__.py +0 -0
  108. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/parsers/claude.py +0 -0
  109. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/peer_executor.py +0 -0
  110. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/peer_executor_windows.py +0 -0
  111. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/peer_reconciliation.py +0 -0
  112. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/peer_reply_outbox.py +0 -0
  113. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/peer_service_host.py +0 -0
  114. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/peer_service_payload.py +0 -0
  115. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/peer_service_reply.py +0 -0
  116. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/peer_staging.py +0 -0
  117. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/scripts/__init__.py +0 -0
  118. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/scripts/dm_notify_filter.py +0 -0
  119. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/scripts/ensure_monitor.sh +0 -0
  120. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/scripts/install_codex_waker_windows.ps1 +0 -0
  121. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/service/__init__.py +0 -0
  122. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/service/app.py +0 -0
  123. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/service/providers.py +0 -0
  124. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/session_bridge.py +0 -0
  125. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/stall_alert.py +0 -0
  126. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/systemd/codex-waker.default +0 -0
  127. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/systemd/peer-reply-drain.default +0 -0
  128. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/systemd/swarph-codex-outbox@.service +0 -0
  129. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/systemd/swarph-codex-outbox@.timer +0 -0
  130. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/systemd/swarph-codex-waker@.service +0 -0
  131. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/systemd/swarph-codex-waker@.timer +0 -0
  132. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/systemd/swarph-peer-reply-drain@.service +0 -0
  133. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/systemd/swarph-peer-reply-drain@.timer +0 -0
  134. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
  135. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
  136. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
  137. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli/tokens.py +0 -0
  138. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
  139. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli.egg-info/entry_points.txt +0 -0
  140. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli.egg-info/requires.txt +0 -0
  141. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/src/swarph_cli.egg-info/top_level.txt +0 -0
  142. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_201_win32_bash_refusal.py +0 -0
  143. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_243_onboard_per_peer_token.py +0 -0
  144. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_307_task4_ask_reply.py +0 -0
  145. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_323_mistral_backend.py +0 -0
  146. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_397_gh_identity_router.py +0 -0
  147. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_397_router_preflight.py +0 -0
  148. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_467b_onboard_operator_credential.py +0 -0
  149. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_523_guide.py +0 -0
  150. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_524_conftest_hermeticity.py +0 -0
  151. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_546_default_gateway.py +0 -0
  152. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_552_codegraph_gateway.py +0 -0
  153. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_555_relay_row_shape.py +0 -0
  154. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_556_relay_freshness.py +0 -0
  155. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_84_cli_labels.py +0 -0
  156. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_add_security_gate.py +0 -0
  157. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_artifact_add.py +0 -0
  158. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_artifact_hash.py +0 -0
  159. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_artifact_lib.py +0 -0
  160. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_artifact_mcp.py +0 -0
  161. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_artifact_skill.py +0 -0
  162. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_artifact_tool.py +0 -0
  163. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_artifact_uri.py +0 -0
  164. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_bench_backends.py +0 -0
  165. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_bench_command.py +0 -0
  166. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_bench_pack.py +0 -0
  167. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_bench_prices.py +0 -0
  168. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_bench_quality.py +0 -0
  169. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_bench_runner.py +0 -0
  170. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_bench_validate.py +0 -0
  171. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_board_card_thread.py +0 -0
  172. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_board_command.py +0 -0
  173. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_board_merge_check.py +0 -0
  174. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_brain_ask_command.py +0 -0
  175. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_brain_ask_env_diagnosis.py +0 -0
  176. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_brain_ask_identity_explicitness.py +0 -0
  177. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_brain_command.py +0 -0
  178. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_caller_meta_guard.py +0 -0
  179. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_capture_lineage.py +0 -0
  180. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_capture_liveness.py +0 -0
  181. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_capture_manifest.py +0 -0
  182. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_capture_paths.py +0 -0
  183. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_capture_security.py +0 -0
  184. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_cell_command_dispatch.py +0 -0
  185. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_cell_harden.py +0 -0
  186. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_cell_loader.py +0 -0
  187. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_cell_selfcheck.py +0 -0
  188. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_cell_verify.py +0 -0
  189. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_chain_token.py +0 -0
  190. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_channel_command.py +0 -0
  191. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_channel_poll_never_breaks_dms.py +0 -0
  192. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_channel_post.py +0 -0
  193. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_channel_wake_policy_filter.py +0 -0
  194. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_chat_command.py +0 -0
  195. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_claude_parser.py +0 -0
  196. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_claude_tmux_template.py +0 -0
  197. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_codegraph_caller_scope.py +0 -0
  198. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_codegraph_command.py +0 -0
  199. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_codegraph_hook.py +0 -0
  200. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_codegraph_mcp.py +0 -0
  201. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_codex_waker.py +0 -0
  202. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_compress_caller_convention.py +0 -0
  203. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_compress_command.py +0 -0
  204. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_compress_levers.py +0 -0
  205. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_compress_marker.py +0 -0
  206. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_compress_verify.py +0 -0
  207. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_console_encoding_e2e.py +0 -0
  208. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_content_composition.py +0 -0
  209. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_cursor_membrane.py +0 -0
  210. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_daemon_command.py +0 -0
  211. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_daemon_console_encoding.py +0 -0
  212. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_daemon_delivery.py +0 -0
  213. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_delivery_queue.py +0 -0
  214. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_display_sanitize.py +0 -0
  215. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_ensure_monitor_no_peer_default.py +0 -0
  216. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_event_emit.py +0 -0
  217. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_feature_to_uri.py +0 -0
  218. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_gateway_brain.py +0 -0
  219. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_gateway_client.py +0 -0
  220. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_gateway_command.py +0 -0
  221. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_gateway_message_idempotency.py +0 -0
  222. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_gh_route_doctor_mesh_name.py +0 -0
  223. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_group_command.py +0 -0
  224. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_heartbeat_check.py +0 -0
  225. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_highlight_command.py +0 -0
  226. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_hook_output.py +0 -0
  227. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_hooks_activity_marker.py +0 -0
  228. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_hooks_add.py +0 -0
  229. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_hooks_bundle.py +0 -0
  230. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_hooks_init.py +0 -0
  231. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_hooks_legacy_generation_removal.py +0 -0
  232. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_hooks_lifecycle.py +0 -0
  233. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_hooks_merge.py +0 -0
  234. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_hooks_verify.py +0 -0
  235. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_hooks_win32_bash_wrapper.py +0 -0
  236. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_hooks_win32_command_path.py +0 -0
  237. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_import_command.py +0 -0
  238. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_inbox_identity_provenance.py +0 -0
  239. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_init_command.py +0 -0
  240. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_init_provider_menu.py +0 -0
  241. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_install_codex_hooks.py +0 -0
  242. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_install_hook.py +0 -0
  243. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_install_multiplexer_command.py +0 -0
  244. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_install_wake_hook.py +0 -0
  245. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_lane_command.py +0 -0
  246. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_main.py +0 -0
  247. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_main_unknown_verb_guard.py +0 -0
  248. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_mcp_server.py +0 -0
  249. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_memory_command.py +0 -0
  250. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_memory_gateway.py +0 -0
  251. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_memory_sync.py +0 -0
  252. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_memory_sync_membrane.py +0 -0
  253. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_mesh_inbox_mark_read.py +0 -0
  254. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_mesh_inbox_sanitize.py +0 -0
  255. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_mesh_send_unknown_peer.py +0 -0
  256. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_mesh_sidecar.py +0 -0
  257. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_mesh_sidecar_cursor_decoupled.py +0 -0
  258. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_mesh_sidecar_wake_survives_read.py +0 -0
  259. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_meta_edge_identity.py +0 -0
  260. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_monitor_channel_poll.py +0 -0
  261. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_monitor_cli.py +0 -0
  262. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_monitor_daemon_collision.py +0 -0
  263. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_monitor_self_name_verification.py +0 -0
  264. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_monitor_state_split.py +0 -0
  265. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_multiplexer.py +0 -0
  266. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_muse_membrane.py +0 -0
  267. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_muse_own_cli.py +0 -0
  268. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_obligation_close_is_surfaced.py +0 -0
  269. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_okf_links.py +0 -0
  270. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_onboard_command.py +0 -0
  271. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_packaged_artifacts_exist.py +0 -0
  272. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_peer_executor.py +0 -0
  273. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_peer_executor_windows.py +0 -0
  274. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_peer_reply_drain_command.py +0 -0
  275. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_peer_reply_drain_supervisor.py +0 -0
  276. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_peer_reply_outbox.py +0 -0
  277. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_peer_reply_transport.py +0 -0
  278. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_peer_service_e2e.py +0 -0
  279. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_peer_service_host.py +0 -0
  280. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_peer_service_payload.py +0 -0
  281. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_peer_service_reply.py +0 -0
  282. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_process_liveness.py +0 -0
  283. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_protocol_handler.py +0 -0
  284. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_providers_isolation.py +0 -0
  285. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_ratify_command.py +0 -0
  286. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_repl_caller_convention.py +0 -0
  287. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_schedule_command.py +0 -0
  288. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_schedule_create_contract.py +0 -0
  289. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_security.py +0 -0
  290. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_service_app.py +0 -0
  291. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_service_command.py +0 -0
  292. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_service_providers.py +0 -0
  293. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_session_bridge_inject.py +0 -0
  294. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_session_bridge_probe.py +0 -0
  295. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_session_bridge_resolve.py +0 -0
  296. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_session_path_role_gate.py +0 -0
  297. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_shipped_units_match_the_cli.py +0 -0
  298. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_smoke_chat.py +0 -0
  299. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_smoke_one_shot.py +0 -0
  300. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_smoke_phase_5_5.py +0 -0
  301. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_spawn_identity_stamp.py +0 -0
  302. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_spawn_live_pin.py +0 -0
  303. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_spawn_mitosis_lineage.py +0 -0
  304. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_spawn_tmux_session.py +0 -0
  305. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_spawn_windows_relaunch.py +0 -0
  306. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_stall_alert.py +0 -0
  307. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_subprocess_decode_encoding.py +0 -0
  308. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_timeline_command.py +0 -0
  309. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_token_file_one_parser.py +0 -0
  310. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_token_precedence_explicit_first.py +0 -0
  311. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_unified_token_resolver.py +0 -0
  312. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_version_consistency.py +0 -0
  313. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_vibe_membrane.py +0 -0
  314. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_watchdog.py +0 -0
  315. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_watchdog_dm_wake.py +0 -0
  316. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
  317. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_watchdog_dm_wake_wiring.py +0 -0
  318. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_watchdog_emit_health.py +0 -0
  319. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_watchdog_install_flags.py +0 -0
  320. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_watchdog_model_rung.py +0 -0
  321. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/tests/test_watchdog_stale_peers.py +0 -0
  322. {swarph_cli-0.45.2 → swarph_cli-0.46.0}/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.2
3
+ Version: 0.46.0
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
@@ -186,6 +186,7 @@ swarph board cards thread <id> [--limit N] # the card's conversation (a card
186
186
  swarph board cards say <id> --content "…" [--to <peer>] [--kind fyi|question|answer|status|unblock]
187
187
  swarph board cards ask <id> <holder> "<what>" [--timeout-hours N] # MINT an obligation:
188
188
  # name who owes what as a ROW, not a sentence (#307)
189
+ swarph board obligations close <id> --outcome <pass|fail|cannot_evaluate> --evidence "what you observed"
189
190
  ```
190
191
 
191
192
  `--project` accepts a numeric id **or** a slug (resolved via `projects list`). Note: card creation always starts at `proposed` (the gateway has no stage-on-create) — use `cards move` to advance.
@@ -138,6 +138,7 @@ swarph board cards thread <id> [--limit N] # the card's conversation (a card
138
138
  swarph board cards say <id> --content "…" [--to <peer>] [--kind fyi|question|answer|status|unblock]
139
139
  swarph board cards ask <id> <holder> "<what>" [--timeout-hours N] # MINT an obligation:
140
140
  # name who owes what as a ROW, not a sentence (#307)
141
+ swarph board obligations close <id> --outcome <pass|fail|cannot_evaluate> --evidence "what you observed"
141
142
  ```
142
143
 
143
144
  `--project` accepts a numeric id **or** a slug (resolved via `projects list`). Note: card creation always starts at `proposed` (the gateway has no stage-on-create) — use `cards move` to advance.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "swarph-cli"
7
- version = "0.45.2"
7
+ version = "0.46.0"
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.2"
19
+ __version__ = "0.46.0"
20
20
 
21
21
  __all__ = ["__version__"]
@@ -284,9 +284,10 @@ def _format_ask(d) -> str:
284
284
  return (f"obligation #{d.get('id')} on card #{d.get('card_id')}: "
285
285
  f"{d.get('holder')} owes it, status={d.get('status')}, {deadline}\n"
286
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)")
287
+ f" thread {d.get('thread_uuid')} — work lands IN that thread; CLOSE "
288
+ f"it with `swarph board obligations close {d.get('id')}` plus an "
289
+ f"outcome and evidence; a plain `mesh send` cannot close it "
290
+ f"(#509/#562 — the VERB is the mechanism, the thread is where it lands)")
290
291
 
291
292
 
292
293
  def _format_thread(data) -> str:
@@ -377,6 +378,25 @@ def _build_parser() -> argparse.ArgumentParser:
377
378
  cr = cards.add_parser("ready", help="flag a card ready-to-advance (move_ready) for the orchestrator")
378
379
  cr.add_argument("id", type=int); cr.add_argument("--clear", action="store_true", help="unset move_ready")
379
380
  cr.add_argument("--json", action="store_true"); _add_common(cr)
381
+
382
+ obl = top.add_parser(
383
+ "obligations",
384
+ help="card obligations — the close act (#562)").add_subparsers(
385
+ dest="command", required=True)
386
+ oc = obl.add_parser(
387
+ "close",
388
+ help="CLOSE an obligation: the DISTINCT ACT that records the check's "
389
+ "outcome plus the evidence observed. A thread reply does NOT "
390
+ "close an obligation (#562) — this does")
391
+ oc.add_argument("id", type=int, help="the obligation id")
392
+ oc.add_argument("--outcome", required=True,
393
+ choices=("pass", "fail", "cannot_evaluate"),
394
+ help="the accept check's OUTCOME")
395
+ oc.add_argument("--evidence", required=True,
396
+ help="what you OBSERVED running the check — whitespace is "
397
+ "refused: whitespace evidence is the vibe-close this "
398
+ "endpoint exists to kill")
399
+ oc.add_argument("--json", action="store_true"); _add_common(oc)
380
400
  return p
381
401
 
382
402
 
@@ -425,6 +445,35 @@ def run_board(argv: list[str]) -> int:
425
445
  st, d = _post_json(f"{gw}/board/projects", _project_add_payload(self_name, args.slug, args.title, goal=args.goal), token)
426
446
  return _out(st, d, lambda x: f"created project #{x.get('id')} ({x.get('slug')})", aj)
427
447
 
448
+ if args.group == "obligations":
449
+ if args.command == "close":
450
+ # #562's CLI half: the explicit close act. The gateway guards the
451
+ # identity (holder, creator, or board orchestrator) and refuses a
452
+ # second close; the CLI's job is to refuse what it can see —
453
+ # whitespace evidence — and to propagate the gateway's refusals
454
+ # with their detail intact.
455
+ evidence = args.evidence
456
+ if not evidence.strip():
457
+ print("swarph board obligations close: --evidence is "
458
+ "whitespace-only — the close act records what you "
459
+ "OBSERVED, and nothing was observed", file=sys.stderr)
460
+ return 2
461
+ st, d = _post_json(
462
+ f"{gw}/board/obligations/{args.id}/close",
463
+ {"outcome": args.outcome, "evidence": evidence}, token)
464
+ if st < 200 or st >= 300:
465
+ detail = d.get("detail", d) if isinstance(d, dict) else d
466
+ print(f"swarph board obligations close: gateway {st}: {_s(detail)}",
467
+ file=sys.stderr)
468
+ return 1
469
+ if aj:
470
+ print(json.dumps(d, indent=2))
471
+ else:
472
+ print(f"obligation #{_s(d.get('id', args.id))} CLOSED by "
473
+ f"{_s(d.get('closed_by', self_name))} — outcome: "
474
+ f"{_s(d.get('close_outcome', args.outcome))}")
475
+ return 0
476
+
428
477
  if args.group == "cards":
429
478
  if args.command == "list":
430
479
  pid, err = _resolve_project(gw, token, args.project)
@@ -51,6 +51,8 @@ def _allow_unchanged() -> int:
51
51
 
52
52
  def run_hook(stdin_text: Optional[str] = None) -> int:
53
53
  raw = stdin_text if stdin_text is not None else sys.stdin.read()
54
+ # PS 5.1 native pipes prefix a (double) UTF-8 BOM; strip before parse.
55
+ raw = (raw or "").lstrip("\ufeff")
54
56
  try:
55
57
  payload = json.loads(raw or "{}")
56
58
  except Exception:
@@ -265,7 +265,13 @@ def run_init(argv: list[str]) -> int:
265
265
  # its own context); an operator running `swarph onboard` first mints the
266
266
  # token into a process that discards it, and the cell's later self-register
267
267
  # returns token_status=existing with nothing.
268
- print(f"next: from the CELL'S OWN context: `swarph mesh register --as {name}` "
268
+ # Scoped as CONDITIONAL (#319 d): a standalone outsider has no mesh and
269
+ # cannot ratify — the mesh path is named for the user who IS joining one,
270
+ # not presented as the next thing everyone must do. The outsider's next
271
+ # step is the `ready:` line above (swarph spawn), which works with zero
272
+ # mesh.
273
+ print(f"next: if you're joining a mesh — from the CELL'S OWN context: "
274
+ f"`swarph mesh register --as {name}` "
269
275
  f"(mints + stores its once-only per-peer token, mode 600); then a witness "
270
276
  f"runs `swarph ratify {name}`. Status at any point: "
271
277
  f"`swarph onboard --check {name}`.")
@@ -17,9 +17,11 @@ A repeat write of the same memory within ``_SUPPRESS_WINDOW`` is suppressed —
17
17
  the timeline is append-only, not append-always. State is a tiny JSON file,
18
18
  best-effort.
19
19
 
20
- Failure-mode invariant: exit 0 on EVERY path, print nothing, never raise. A
21
- hook that fails a tool result because the timeline is unreachable inverts every
22
- priority this system has.
20
+ Failure-mode invariant: exit 0 on EVERY path, never raise. The hook prints
21
+ nothing OF ITS OWN; the shared ``_log_via_gateway`` prints its one-line
22
+ "logged -> TIMELINE.md" on success (cursor-win, 2026-08-22: the invariant was
23
+ misdescribed, not violated). A hook that fails a tool result because the
24
+ timeline is unreachable inverts every priority this system has.
23
25
  """
24
26
 
25
27
  from __future__ import annotations
@@ -108,7 +110,12 @@ def _mark_emitted(slug: str) -> None:
108
110
 
109
111
 
110
112
  def _cell() -> str:
111
- return (os.environ.get("SWARPH_CELL") or os.environ.get("SWARPH_SELF")
113
+ # SWARPH_SELF outranks SWARPH_CELL — the house order (#332: a self-identity
114
+ # statement outranks an ambient one), and the live reason is #538: psmux
115
+ # leaks SWARPH_CELL from the spawning environment, so a CELL-first order
116
+ # posts this cell's highlight under ANOTHER cell's name and token
117
+ # (measured by cursor-win on the Windows membrane, 2026-08-22).
118
+ return (os.environ.get("SWARPH_SELF") or os.environ.get("SWARPH_CELL")
112
119
  or socket.gethostname())
113
120
 
114
121
 
@@ -117,7 +124,10 @@ def run_memory_emit_hook(argv: list[str] | None = None) -> int:
117
124
  raw = ""
118
125
  if not sys.stdin.isatty():
119
126
  raw = sys.stdin.read()
120
- payload = json.loads(raw) if raw.strip() else {}
127
+ # PS 5.1 native pipes prefix a (double) UTF-8 BOM — json.loads raises,
128
+ # and the failure invariant converts that into an INVISIBLE no-op
129
+ # (measured by cursor-win, 2026-08-22). Strip before parse.
130
+ payload = json.loads(raw.lstrip("\ufeff")) if raw.strip() else {}
121
131
  except Exception:
122
132
  return 0
123
133
 
@@ -28,6 +28,8 @@ import sys
28
28
  import time
29
29
  import urllib.error
30
30
  import urllib.parse
31
+
32
+ import swarph_cli
31
33
  import urllib.request
32
34
  from collections import deque
33
35
  from collections.abc import MutableMapping
@@ -82,7 +84,8 @@ def _build_parser() -> argparse.ArgumentParser:
82
84
  _add_common(send)
83
85
 
84
86
  reply = sub.add_parser(
85
- "reply", help="reply to a DM IN ITS OWN THREAD (closes a #307 obligation)")
87
+ "reply", help="reply to a DM IN ITS OWN THREAD (does NOT close the "
88
+ "obligation — the close act is `board obligations close`, #562)")
86
89
  reply.add_argument("message_id", type=int, help="the DM being replied to")
87
90
  reply.add_argument("--kind", default="answer",
88
91
  help="message kind (default: answer). NOT gated — a reply is "
@@ -136,8 +139,42 @@ def _build_parser() -> argparse.ArgumentParser:
136
139
  action="store_true",
137
140
  help="allow register when a local per-peer token file already exists",
138
141
  )
142
+ register.add_argument(
143
+ "--replace",
144
+ action="store_true",
145
+ help="REPLACE the advertised capability blob with exactly the "
146
+ "--capability keys given (sends full=true). Default is MERGE: "
147
+ "the peer's currently-registered keys are read first and "
148
+ "re-submitted, so updating one field does not destroy the rest "
149
+ "(#124).",
150
+ )
139
151
  _add_common(register)
140
152
 
153
+ peers = sub.add_parser(
154
+ "peers",
155
+ help="list registered peers with their reported swarph_cli_version; "
156
+ "--stale-than X names only peers REPORTING older than X, and "
157
+ "names the UNREPORTED separately (#535)",
158
+ )
159
+ peers.add_argument(
160
+ "--stale-than", default=None, metavar="VERSION",
161
+ help="list only peers whose reported swarph_cli_version is older "
162
+ "than VERSION; peers with no reported version are named "
163
+ "UNREPORTED, never counted as current",
164
+ )
165
+ peers.add_argument(
166
+ "--as", dest="self_name", default=None,
167
+ help="optional peer identity. IF OMITTED the ambient shared token "
168
+ "(MESH_GATEWAY_TOKEN) is used — a registry read does not require "
169
+ "a self identity. Pass --as to use a stored per-peer token instead.",
170
+ )
171
+ peers.add_argument(
172
+ "--gateway",
173
+ default=os.environ.get("MESH_GATEWAY_URL", _DEFAULT_GATEWAY),
174
+ help="mesh-gateway base URL",
175
+ )
176
+ peers.add_argument("--token-file", default=None, help="explicit bearer token file")
177
+
141
178
  sidecar = sub.add_parser("sidecar", help="poll inbox and wake a tmux cell")
142
179
  sidecar.add_argument("--tmux-target", default=None, help="tmux target pane")
143
180
  sidecar.add_argument("--state-dir", default=None, help="state directory")
@@ -526,12 +563,15 @@ def _find_inbox_message(gateway: str, token: str, self_name: str,
526
563
 
527
564
 
528
565
  def _run_reply(args: argparse.Namespace) -> int:
529
- """Reply to a DM IN ITS THREAD, which is what lets #307 close an obligation.
566
+ """Reply to a DM IN ITS THREAD. Since #562 this does NOT close the
567
+ obligation — closing is the distinct act `board obligations close`
568
+ (POST /board/obligations/{id}/close); the reply path only REMINDS via
569
+ the open_obligations field.
530
570
 
531
571
  >>> A REPLY IS UNIVERSAL ACROSS KINDS, DELIBERATELY. <<< The spec's constraint:
532
572
  not gated by kind. Gating would mean a peer could owe you an answer on a DM whose
533
- kind nobody thought to allow, and the obligation would be unclosable through the
534
- product — the exact "waiting on a seat" shape #307 exists to kill.
573
+ kind nobody thought to allow, and the obligation would be unanswerable through
574
+ the product — the exact "waiting on a seat" shape #307 exists to kill.
535
575
 
536
576
  >>> AND A THREADLESS REPLY MUST NOT REPORT THE SAME LINE AS A THREADED ONE. <<<
537
577
  Most DMs carry no thread. Refusing them would make this verb useless for the
@@ -776,11 +816,43 @@ def _run_register(args: argparse.Namespace) -> int:
776
816
  for spec in args.capability:
777
817
  key, value = _parse_capability(spec)
778
818
  caps[key] = value
819
+ # >>> #124, CLIENT HALF: MERGE OVER THE STORED BLOB BY DEFAULT. <<< The
820
+ # gateway replaces capabilities wholesale, so submitting only the keys
821
+ # this invocation cares about silently destroys the rest (gemini-
822
+ # researcher's role/agent_type, lost to a model_default update). Read
823
+ # the peer's currently-registered blob and re-submit it merged:
824
+ # submitted keys override, stored-only keys survive. --replace opts into
825
+ # the deliberate wholesale replace (full=true, which the gateway's #124
826
+ # guard accepts without its 409). A register that CANNOT READ the
827
+ # registry — first registration, gateway unreachable — proceeds with
828
+ # what it has: the read must never BLOCK the write; there is nothing to
829
+ # destroy on a row that does not exist yet.
830
+ gateway = args.gateway.rstrip("/")
831
+ stored_caps: dict = {}
832
+ found_stored_caps = False
833
+ if not args.replace:
834
+ gstatus, gpayload = _http_get_json(f"{gateway}/peers/{self_name}", token)
835
+ if gstatus == 200 and isinstance(gpayload.get("capabilities"), dict):
836
+ stored_caps = gpayload["capabilities"]
837
+ found_stored_caps = True
838
+ if args.replace:
839
+ merged = caps or {"can_claim_tasks": True}
840
+ elif found_stored_caps or caps:
841
+ merged = {**stored_caps, **caps}
842
+ else:
843
+ merged = {"can_claim_tasks": True}
844
+ # #535: report what version this cell RUNS, on every register — a
845
+ # SUBMITTED key, so the merge refreshes it on re-register and preserves
846
+ # it across partial updates. Without a stored version field, "who is
847
+ # stale" is unanswerable and "the fix propagates" is unfalsifiable.
848
+ merged["swarph_cli_version"] = swarph_cli.__version__
779
849
  body = {
780
850
  "name": self_name,
781
851
  "url": args.url or f"http://{self_name}:8787",
782
- "capabilities": caps or {"can_claim_tasks": True},
852
+ "capabilities": merged,
783
853
  }
854
+ if args.replace:
855
+ body["full"] = True
784
856
  status, payload = _post_json(
785
857
  f"{args.gateway.rstrip('/')}/peers/register",
786
858
  body,
@@ -806,6 +878,96 @@ def _run_register(args: argparse.Namespace) -> int:
806
878
  return 0
807
879
 
808
880
 
881
+ def _version_tuple(text: object) -> Optional[tuple[int, ...]]:
882
+ """Parse "0.45.1" into (0, 45, 1). Returns None for anything that is not
883
+ a dotted-numeric version — an unparseable version is UNMEASURABLE, not
884
+ current (#535's falsifier clause)."""
885
+ if not isinstance(text, str):
886
+ return None
887
+ parts = text.strip().split(".")
888
+ if not parts or any(not p.isdigit() for p in parts):
889
+ return None
890
+ return tuple(int(p) for p in parts)
891
+
892
+
893
+ def _version_is_stale(reported: object, cutoff: str) -> Optional[bool]:
894
+ """True = reported is older than cutoff; False = current-or-newer;
895
+ None = unmeasurable (missing/unparseable). Numeric tuple compare, so
896
+ 0.45.10 > 0.45.9 — lexicographic would call 0.9.0 newer than 0.10.0."""
897
+ have, want = _version_tuple(reported), _version_tuple(cutoff)
898
+ if have is None or want is None:
899
+ return None
900
+ width = max(len(have), len(want))
901
+ return have + (0,) * (width - len(have)) < want + (0,) * (width - len(want))
902
+
903
+
904
+ def _run_peers(args: argparse.Namespace) -> int:
905
+ """#535: answer "which peers run older than X" from the STORED field —
906
+ never by asking each cell out of band, never by assuming.
907
+
908
+ A registry READ does not require a self identity: an operator asking
909
+ "who is stale" on any box with the shared token is not acting AS a
910
+ cell. --as still works (its peer token outranks the ambient one)."""
911
+ from swarph_cli.tokens import resolve_token
912
+ resolution = resolve_token(args.self_name, args.token_file,
913
+ identity_is_explicit=bool(args.self_name))
914
+ if resolution is None:
915
+ print("swarph mesh peers: no token (set MESH_GATEWAY_TOKEN, pass "
916
+ "--token-file, or --as a peer with a stored token)",
917
+ file=sys.stderr)
918
+ return 2
919
+ token = resolution.token
920
+ gateway = args.gateway.rstrip("/")
921
+ status, payload = _http_get_json(f"{gateway}/peers", token)
922
+ if status < 200 or status >= 300:
923
+ detail = payload.get("detail", "<gateway error>") if isinstance(payload, dict) else "<gateway error>"
924
+ print(f"swarph mesh peers: gateway {status}: {detail}", file=sys.stderr)
925
+ return 1
926
+ peers = payload.get("peers") if isinstance(payload, dict) else None
927
+ if not isinstance(peers, list):
928
+ print("swarph mesh peers: unexpected /peers shape (no peer list)",
929
+ file=sys.stderr)
930
+ return 1
931
+
932
+ cutoff = args.stale_than
933
+ if cutoff is not None and _version_tuple(cutoff) is None:
934
+ print(f"swarph mesh peers: --stale-than {cutoff!r} is not a "
935
+ f"dotted-numeric version", file=sys.stderr)
936
+ return 2
937
+
938
+ stale, unreported, current = [], [], []
939
+ for p in peers:
940
+ if not isinstance(p, dict) or not isinstance(p.get("name"), str):
941
+ continue
942
+ caps = p.get("capabilities")
943
+ version = caps.get("swarph_cli_version") if isinstance(caps, dict) else None
944
+ version_is_valid = _version_tuple(version) is not None
945
+ verdict = _version_is_stale(version, cutoff) if cutoff else None
946
+ if cutoff and verdict is True:
947
+ stale.append((p["name"], version))
948
+ elif not version_is_valid:
949
+ unreported.append(p["name"])
950
+ else:
951
+ current.append((p["name"], version))
952
+
953
+ if cutoff:
954
+ for name, version in stale:
955
+ print(f"STALE {name} reports {version} (< {cutoff})")
956
+ for name in unreported:
957
+ # Named, never silently counted as current: absence of the field
958
+ # is unmeasurability, not health — the falsifier clause.
959
+ print(f"UNREPORTED {name} (no swarph_cli_version — unmeasurable, "
960
+ f"NOT known-current)")
961
+ print(f"{len(stale)} stale, {len(unreported)} unreported, "
962
+ f"{len(current)} current (cutoff {cutoff})")
963
+ else:
964
+ for name, version in current:
965
+ print(f"{name} {version or 'UNREPORTED'}")
966
+ for name in unreported:
967
+ print(f"{name} UNREPORTED")
968
+ return 0
969
+
970
+
809
971
  def _default_sidecar_state_dir(self_name: str) -> Path:
810
972
  # NOT renamed to .../monitor when `swarph monitor` shipped (card #122).
811
973
  # `swarph init` pins this exact path as the cell's cursor_path and the
@@ -1937,6 +2099,8 @@ def run_mesh(argv: list[str]) -> int:
1937
2099
  return _run_inbox(args)
1938
2100
  if args.command == "register":
1939
2101
  return _run_register(args)
2102
+ if args.command == "peers":
2103
+ return _run_peers(args)
1940
2104
  if args.command == "sidecar":
1941
2105
  return _run_sidecar(args)
1942
2106
  parser.error(f"unknown command: {args.command}")
@@ -828,6 +828,7 @@ def _register_capabilities(self_name: str, gateway: str, token_file, caps: dict)
828
828
  url=None,
829
829
  capability=[f"{k}={json.dumps(v)}" for k, v in caps.items()],
830
830
  force=True,
831
+ replace=False,
831
832
  gateway=gateway,
832
833
  token_file=token_file,
833
834
  )
@@ -427,7 +427,7 @@ def _resolve_token(token_file_arg: Optional[str], *,
427
427
  " To stand up YOUR OWN instead, no token required:\n"
428
428
  " pip install 'swarph-cli[gateway]'\n"
429
429
  " swarph gateway serve --port 8788 --db ~/.swarph/mesh.db\n"
430
- " swarph init <name> # scaffold a cell\n"
430
+ " swarph init <name> --provider <provider> # scaffold a cell\n"
431
431
  )
432
432
 
433
433