swarph-cli 0.41.2__tar.gz → 0.41.4__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 (230) hide show
  1. {swarph_cli-0.41.2/src/swarph_cli.egg-info → swarph_cli-0.41.4}/PKG-INFO +1 -1
  2. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/pyproject.toml +1 -1
  3. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/__init__.py +1 -1
  4. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/board.py +57 -6
  5. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/board_merge_check.py +32 -6
  6. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/daemon.py +15 -1
  7. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/onboard.py +44 -7
  8. {swarph_cli-0.41.2 → swarph_cli-0.41.4/src/swarph_cli.egg-info}/PKG-INFO +1 -1
  9. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli.egg-info/SOURCES.txt +2 -0
  10. swarph_cli-0.41.4/tests/test_243_onboard_per_peer_token.py +86 -0
  11. swarph_cli-0.41.4/tests/test_84_cli_labels.py +81 -0
  12. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_board_merge_check.py +47 -0
  13. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_brain_ask_command.py +1 -1
  14. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_onboard_command.py +23 -7
  15. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_watchdog.py +1 -1
  16. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/LICENSE +0 -0
  17. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/README.md +0 -0
  18. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/setup.cfg +0 -0
  19. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/bench/__init__.py +0 -0
  20. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/bench/backends.py +0 -0
  21. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/bench/pack.py +0 -0
  22. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/bench/prices.py +0 -0
  23. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/bench/quality.py +0 -0
  24. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/bench/refresh_prices.py +0 -0
  25. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/bench/runner.py +0 -0
  26. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/bench/validate.py +0 -0
  27. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/caller.py +0 -0
  28. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/capture/__init__.py +0 -0
  29. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/capture/harden.py +0 -0
  30. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/capture/lineage.py +0 -0
  31. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/capture/liveness.py +0 -0
  32. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/capture/manifest.py +0 -0
  33. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/capture/paths.py +0 -0
  34. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/capture/verify.py +0 -0
  35. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/cell.py +0 -0
  36. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/chain_token.py +0 -0
  37. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/__init__.py +0 -0
  38. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/_display.py +0 -0
  39. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/_gateway_client.py +0 -0
  40. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/add.py +0 -0
  41. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/bench.py +0 -0
  42. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/brain.py +0 -0
  43. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/brain_ask.py +0 -0
  44. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/cell.py +0 -0
  45. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/cell_selfcheck.py +0 -0
  46. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/channel.py +0 -0
  47. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/chat.py +0 -0
  48. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/codegraph.py +0 -0
  49. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/codegraph_hook.py +0 -0
  50. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/compress.py +0 -0
  51. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/event.py +0 -0
  52. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/gateway.py +0 -0
  53. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/group.py +0 -0
  54. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/highlight.py +0 -0
  55. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/hook_output.py +0 -0
  56. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/hooks.py +0 -0
  57. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/import_session.py +0 -0
  58. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/init.py +0 -0
  59. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/install_hook.py +0 -0
  60. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/install_multiplexer.py +0 -0
  61. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/lane.py +0 -0
  62. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/mcp_server.py +0 -0
  63. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/memory.py +0 -0
  64. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/memory_sync.py +0 -0
  65. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/mesh.py +0 -0
  66. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/monitor.py +0 -0
  67. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/okf_links.py +0 -0
  68. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/protocol_handler.py +0 -0
  69. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/ratify.py +0 -0
  70. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/schedule.py +0 -0
  71. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/security.py +0 -0
  72. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/service.py +0 -0
  73. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/spawn.py +0 -0
  74. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/timeline.py +0 -0
  75. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/commands/watchdog.py +0 -0
  76. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/compress/__init__.py +0 -0
  77. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/compress/levers.py +0 -0
  78. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/compress/marker.py +0 -0
  79. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/compress/verify.py +0 -0
  80. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/console_safe.py +0 -0
  81. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/delivery_queue.py +0 -0
  82. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/gateway/__init__.py +0 -0
  83. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/gateway/feature_registry.py +0 -0
  84. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/gateway/lanes_control.py +0 -0
  85. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/gateway/schema.sql +0 -0
  86. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/gateway/server.py +0 -0
  87. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/gateway/services_control.py +0 -0
  88. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/main.py +0 -0
  89. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/multiplexer.py +0 -0
  90. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/parsers/__init__.py +0 -0
  91. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/parsers/claude.py +0 -0
  92. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/scripts/ensure_monitor.sh +0 -0
  93. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/service/__init__.py +0 -0
  94. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/service/app.py +0 -0
  95. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/service/providers.py +0 -0
  96. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/session_bridge.py +0 -0
  97. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/stall_alert.py +0 -0
  98. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
  99. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
  100. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
  101. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
  102. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli.egg-info/entry_points.txt +0 -0
  103. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli.egg-info/requires.txt +0 -0
  104. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/src/swarph_cli.egg-info/top_level.txt +0 -0
  105. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_add_security_gate.py +0 -0
  106. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_artifact_add.py +0 -0
  107. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_artifact_hash.py +0 -0
  108. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_artifact_lib.py +0 -0
  109. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_artifact_mcp.py +0 -0
  110. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_artifact_skill.py +0 -0
  111. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_artifact_tool.py +0 -0
  112. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_artifact_uri.py +0 -0
  113. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_bench_backends.py +0 -0
  114. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_bench_command.py +0 -0
  115. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_bench_pack.py +0 -0
  116. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_bench_prices.py +0 -0
  117. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_bench_quality.py +0 -0
  118. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_bench_runner.py +0 -0
  119. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_bench_validate.py +0 -0
  120. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_board_command.py +0 -0
  121. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_brain_ask_env_diagnosis.py +0 -0
  122. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_brain_command.py +0 -0
  123. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_caller_meta_guard.py +0 -0
  124. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_capture_lineage.py +0 -0
  125. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_capture_liveness.py +0 -0
  126. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_capture_manifest.py +0 -0
  127. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_capture_paths.py +0 -0
  128. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_capture_security.py +0 -0
  129. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_cell_command_dispatch.py +0 -0
  130. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_cell_harden.py +0 -0
  131. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_cell_loader.py +0 -0
  132. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_cell_selfcheck.py +0 -0
  133. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_cell_verify.py +0 -0
  134. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_chain_token.py +0 -0
  135. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_channel_command.py +0 -0
  136. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_channel_post.py +0 -0
  137. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_chat_command.py +0 -0
  138. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_claude_parser.py +0 -0
  139. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_claude_tmux_template.py +0 -0
  140. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_codegraph_caller_scope.py +0 -0
  141. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_codegraph_command.py +0 -0
  142. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_codegraph_hook.py +0 -0
  143. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_codegraph_mcp.py +0 -0
  144. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_compress_caller_convention.py +0 -0
  145. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_compress_command.py +0 -0
  146. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_compress_levers.py +0 -0
  147. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_compress_marker.py +0 -0
  148. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_compress_verify.py +0 -0
  149. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_console_encoding_e2e.py +0 -0
  150. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_daemon_command.py +0 -0
  151. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_daemon_console_encoding.py +0 -0
  152. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_daemon_delivery.py +0 -0
  153. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_delivery_queue.py +0 -0
  154. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_display_sanitize.py +0 -0
  155. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_event_emit.py +0 -0
  156. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_feature_to_uri.py +0 -0
  157. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_gateway_brain.py +0 -0
  158. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_gateway_client.py +0 -0
  159. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_gateway_command.py +0 -0
  160. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_group_command.py +0 -0
  161. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_highlight_command.py +0 -0
  162. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_hook_output.py +0 -0
  163. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_hooks_activity_marker.py +0 -0
  164. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_hooks_add.py +0 -0
  165. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_hooks_bundle.py +0 -0
  166. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_hooks_init.py +0 -0
  167. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_hooks_lifecycle.py +0 -0
  168. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_hooks_merge.py +0 -0
  169. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_import_command.py +0 -0
  170. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_init_command.py +0 -0
  171. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_install_hook.py +0 -0
  172. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_install_multiplexer_command.py +0 -0
  173. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_lane_command.py +0 -0
  174. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_main.py +0 -0
  175. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_main_unknown_verb_guard.py +0 -0
  176. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_mcp_server.py +0 -0
  177. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_memory_command.py +0 -0
  178. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_memory_gateway.py +0 -0
  179. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_memory_sync.py +0 -0
  180. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_memory_sync_membrane.py +0 -0
  181. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_mesh_command.py +0 -0
  182. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_mesh_inbox_mark_read.py +0 -0
  183. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_mesh_inbox_sanitize.py +0 -0
  184. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_mesh_sidecar.py +0 -0
  185. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_mesh_sidecar_cursor_decoupled.py +0 -0
  186. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_mesh_sidecar_wake_survives_read.py +0 -0
  187. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_meta_edge_identity.py +0 -0
  188. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_monitor_cli.py +0 -0
  189. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_monitor_daemon_collision.py +0 -0
  190. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_monitor_self_name_verification.py +0 -0
  191. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_monitor_state_split.py +0 -0
  192. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_multiplexer.py +0 -0
  193. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_okf_links.py +0 -0
  194. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_packaged_artifacts_exist.py +0 -0
  195. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_process_liveness.py +0 -0
  196. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_protocol_handler.py +0 -0
  197. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_providers_isolation.py +0 -0
  198. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_ratify_command.py +0 -0
  199. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_repl_caller_convention.py +0 -0
  200. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_schedule_command.py +0 -0
  201. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_schedule_create_contract.py +0 -0
  202. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_security.py +0 -0
  203. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_service_app.py +0 -0
  204. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_service_command.py +0 -0
  205. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_service_providers.py +0 -0
  206. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_session_bridge_inject.py +0 -0
  207. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_session_bridge_probe.py +0 -0
  208. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_session_bridge_resolve.py +0 -0
  209. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_session_path_role_gate.py +0 -0
  210. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_shipped_units_match_the_cli.py +0 -0
  211. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_smoke_chat.py +0 -0
  212. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_smoke_one_shot.py +0 -0
  213. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_smoke_phase_5_5.py +0 -0
  214. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_spawn_base_hoist.py +0 -0
  215. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_spawn_command.py +0 -0
  216. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_spawn_live_pin.py +0 -0
  217. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_spawn_mitosis_lineage.py +0 -0
  218. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_spawn_tmux_session.py +0 -0
  219. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_spawn_windows_relaunch.py +0 -0
  220. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_stall_alert.py +0 -0
  221. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_timeline_command.py +0 -0
  222. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_token_file_one_parser.py +0 -0
  223. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_version_consistency.py +0 -0
  224. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_watchdog_dm_wake.py +0 -0
  225. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
  226. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_watchdog_dm_wake_wiring.py +0 -0
  227. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_watchdog_emit_health.py +0 -0
  228. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_watchdog_install_flags.py +0 -0
  229. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_watchdog_model_rung.py +0 -0
  230. {swarph_cli-0.41.2 → swarph_cli-0.41.4}/tests/test_watchdog_stale_peers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swarph-cli
3
- Version: 0.41.2
3
+ Version: 0.41.4
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`, 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.41.2"
7
+ version = "0.41.4"
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`, 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.41.2"
19
+ __version__ = "0.41.4"
20
20
 
21
21
  __all__ = ["__version__"]
@@ -50,20 +50,43 @@ def _patch_json(url: str, body: dict, token: str, *, timeout: float = 10.0) -> t
50
50
 
51
51
  # ── pure builders (unit-tested) ───────────────────────────────────────────────
52
52
 
53
- def _cards_list_url(gateway: str, *, project=None, stage=None, assignee=None) -> str:
54
- q = {k: v for k, v in (("project", project), ("stage", stage), ("assignee", assignee)) if v}
53
+ def _cards_list_url(gateway: str, *, project=None, stage=None, assignee=None,
54
+ label=None) -> str:
55
+ q = {k: v for k, v in (("project", project), ("stage", stage),
56
+ ("assignee", assignee), ("label", label)) if v}
55
57
  base = f"{gateway.rstrip('/')}/board/cards"
56
58
  return f"{base}?{urllib.parse.urlencode(q)}" if q else base
57
59
 
58
60
 
59
- def _card_add_payload(actor, project_id, title, *, body=None, ai2=False, priority=0) -> dict:
61
+ def _card_add_payload(actor, project_id, title, *, body=None, ai2=False, priority=0,
62
+ labels=None) -> dict:
60
63
  p = {"actor": actor, "project_id": int(project_id), "title": title,
61
64
  "ai2": bool(ai2), "priority": int(priority)}
62
65
  if body:
63
66
  p["body"] = body
67
+ if labels:
68
+ p["labels"] = list(labels)
64
69
  return p
65
70
 
66
71
 
72
+ def _apply_label(current: list, action: str, label: str) -> list:
73
+ """Compute the NEW label set. Pure, so the read-modify-write is testable
74
+ without a gateway.
75
+
76
+ The server takes a FULL REPLACEMENT (labels are a set — a merge-only field
77
+ could never REMOVE one), so `label rm` is unavoidably read-modify-write. That
78
+ is last-writer-wins between two simultaneous editors of the SAME card, which
79
+ the board already has for `stage` and `assignee` — stated rather than hidden,
80
+ because the alternative (server-side add/rm ops) is a bigger surface than
81
+ this card justifies.
82
+ """
83
+ cur = [str(x).strip().lower() for x in (current or [])]
84
+ lab = label.strip().lower()
85
+ if action == "add":
86
+ return cur if lab in cur else cur + [lab]
87
+ return [x for x in cur if x != lab]
88
+
89
+
67
90
  def _project_add_payload(actor, slug, title, *, goal=None) -> dict:
68
91
  p = {"actor": actor, "slug": slug, "title": title}
69
92
  if goal:
@@ -153,13 +176,17 @@ def _build_parser() -> argparse.ArgumentParser:
153
176
  cards = top.add_parser("cards", help="board cards").add_subparsers(dest="command", required=True)
154
177
  cl = cards.add_parser("list", help="list cards")
155
178
  cl.add_argument("--project"); cl.add_argument("--stage"); cl.add_argument("--assignee")
179
+ cl.add_argument("--label", help="only cards carrying this label (exact match)")
156
180
  cl.add_argument("--json", action="store_true"); _add_common(cl)
157
181
  cs = cards.add_parser("show", help="show one card"); cs.add_argument("id", type=int)
158
182
  cs.add_argument("--json", action="store_true"); _add_common(cs)
159
183
  ca = cards.add_parser("add", help="create a card")
160
184
  ca.add_argument("--project", required=True, help="project id or slug"); ca.add_argument("--title", required=True)
161
185
  ca.add_argument("--body"); ca.add_argument("--ai2", action="store_true")
162
- ca.add_argument("--priority", type=int, default=0); ca.add_argument("--json", action="store_true"); _add_common(ca)
186
+ ca.add_argument("--priority", type=int, default=0)
187
+ ca.add_argument("--label", action="append", dest="labels", metavar="LABEL",
188
+ help="attach a label (repeatable)")
189
+ ca.add_argument("--json", action="store_true"); _add_common(ca)
163
190
  cm = cards.add_parser("move", help="move a card to a stage")
164
191
  cm.add_argument("id", type=int); cm.add_argument("stage"); cm.add_argument("--json", action="store_true"); _add_common(cm)
165
192
  ck = cards.add_parser("link", help="add/update a link on a card (merges)")
@@ -167,6 +194,10 @@ def _build_parser() -> argparse.ArgumentParser:
167
194
  ck.add_argument("--json", action="store_true"); _add_common(ck)
168
195
  cn = cards.add_parser("assign", help="set a card's assignee")
169
196
  cn.add_argument("id", type=int); cn.add_argument("assignee"); cn.add_argument("--json", action="store_true"); _add_common(cn)
197
+ cb = cards.add_parser("label", help="add/remove a label on a card")
198
+ cb.add_argument("action", choices=["add", "rm"])
199
+ cb.add_argument("id", type=int); cb.add_argument("label")
200
+ cb.add_argument("--json", action="store_true"); _add_common(cb)
170
201
  cr = cards.add_parser("ready", help="flag a card ready-to-advance (move_ready) for the orchestrator")
171
202
  cr.add_argument("id", type=int); cr.add_argument("--clear", action="store_true", help="unset move_ready")
172
203
  cr.add_argument("--json", action="store_true"); _add_common(cr)
@@ -224,7 +255,9 @@ def run_board(argv: list[str]) -> int:
224
255
  if err:
225
256
  print(f"swarph board: {err}", file=sys.stderr)
226
257
  return 1
227
- st, d = _http_get_json(_cards_list_url(gw, project=pid, stage=args.stage, assignee=args.assignee), token)
258
+ st, d = _http_get_json(_cards_list_url(gw, project=pid, stage=args.stage,
259
+ assignee=args.assignee,
260
+ label=getattr(args, "label", None)), token)
228
261
  return _out(st, d, _format_cards, aj)
229
262
  if args.command == "show":
230
263
  st, d = _http_get_json(f"{gw}/board/cards/{args.id}", token)
@@ -234,8 +267,26 @@ def run_board(argv: list[str]) -> int:
234
267
  if err or pid is None:
235
268
  print(f"swarph board: {err or 'project required'}", file=sys.stderr)
236
269
  return 1
237
- st, d = _post_json(f"{gw}/board/cards", _card_add_payload(self_name, pid, args.title, body=args.body, ai2=args.ai2, priority=args.priority), token)
270
+ st, d = _post_json(f"{gw}/board/cards", _card_add_payload(
271
+ self_name, pid, args.title, body=args.body, ai2=args.ai2,
272
+ priority=args.priority, labels=getattr(args, "labels", None)), token)
238
273
  return _out(st, d, lambda x: f"created card #{x.get('id')} [{x.get('stage')}] (stage defaults to proposed — use `cards move` to advance)", aj)
274
+ if args.command == "label":
275
+ # READ the current set before replacing it. If the GET fails we must
276
+ # NOT write — a PATCH built on a failed read would silently CLOBBER
277
+ # every other label on the card. Refuse loudly instead.
278
+ st, cur = _http_get_json(f"{gw}/board/cards/{args.id}", token)
279
+ if st != 200 or not isinstance(cur, dict):
280
+ print(f"swarph board: cannot read card #{args.id} to modify its "
281
+ f"labels (HTTP {st}) — refusing to write, a replacement built "
282
+ f"on a failed read would drop the card's other labels",
283
+ file=sys.stderr)
284
+ return 1
285
+ new = _apply_label(cur.get("labels") or [], args.action, args.label)
286
+ st, d = _patch_json(f"{gw}/board/cards/{args.id}",
287
+ {"actor": self_name, "labels": new}, token)
288
+ return _out(st, d, lambda x: f"card #{x.get('id')} labels -> "
289
+ f"{', '.join(x.get('labels') or []) or '(none)'}", aj)
239
290
  if args.command == "move":
240
291
  st, d = _patch_json(f"{gw}/board/cards/{args.id}", {"actor": self_name, "stage": args.stage}, token)
241
292
  return _out(st, d, lambda x: f"card #{x.get('id')} -> {x.get('stage')}", aj)
@@ -161,15 +161,41 @@ def decide(card: dict, pr_state: dict) -> Decision:
161
161
  # vacuous for exactly the backlog it must cover. ok=None => COULD NOT
162
162
  # EVALUATE => REFUSE, which is this file's existing doctrine for the
163
163
  # unreachable-GitHub case and the reason it has a tri-state at all.
164
+ # >>> A STAMP THAT EXISTS AND LACKS THE KEY IS AS BLIND AS NO STAMP AT ALL,
165
+ # AND IT IS 92% OF PRODUCTION. <<< (droplet, running decide() on the
166
+ # INSTALLED 0.41.2 rather than reading it.)
167
+ #
168
+ # The first version tested `_vstamp is None` — ABSENCE OF THE STAMP. The
169
+ # backlog's actual shape is a stamp that EXISTS (written before
170
+ # self_authored shipped) and lacks the KEY. That fell through to the
171
+ # comparison, where `.get("self_authored")` returns None, `not None` is
172
+ # True, and the leg reported ok=True. A textbook self-review PASSED.
173
+ #
174
+ # MEASURED: 53 link_stamps in production, 4 carrying the flag — 49 (92%)
175
+ # are the middle shape. So the leg was VACUOUS FOR EXACTLY THE BACKLOG IT
176
+ # WAS BUILT FOR: the outcome the design explicitly targets, and which I
177
+ # reported to the date-holder as avoided. He authorised near-total
178
+ # ABSTENTION on the backlog; what shipped was near-total PASS — the
179
+ # opposite, and MORE PERMISSIVE THAN THE PHASE-1 STATE IT REPLACES.
180
+ #
181
+ # The two blind causes are kept DISTINCT in the detail because they decay
182
+ # differently: a missing stamp is a producer that dropped it (an outage
183
+ # shape); a missing key is a verdict older than the flag (a shape that
184
+ # drains to zero as verdicts are rewritten). A single message would make
185
+ # them indistinguishable, which is the naming rule one level down.
164
186
  _vstamp = (card.get("link_stamps") or {}).get("peer_verdict")
165
- if _vstamp is None:
187
+ if _vstamp is None or "self_authored" not in _vstamp:
188
+ _why = ("no link_stamp for peer_verdict at all — a producer dropped it, "
189
+ "or the verdict predates #199"
190
+ if _vstamp is None else
191
+ f"link_stamp exists (by={_vstamp.get('by')!r}) but carries NO "
192
+ f"self_authored key — the verdict predates the flag")
166
193
  add("verdict is not self-authored", None,
167
- "no link_stamp for peer_verdict — cannot tell who wrote it "
168
- "(pre-#199 verdict, or a producer that dropped link_stamps); "
169
- "COULD NOT EVALUATE, never a pass")
194
+ f"{_why}; cannot tell whether the reviewee wrote it. "
195
+ f"COULD NOT EVALUATE, never a pass")
170
196
  else:
171
- add("verdict is not self-authored", not _vstamp.get("self_authored"),
172
- f"self_authored={_vstamp.get('self_authored')!r} by={_vstamp.get('by')!r} "
197
+ add("verdict is not self-authored", not _vstamp["self_authored"],
198
+ f"self_authored={_vstamp['self_authored']!r} by={_vstamp.get('by')!r} "
173
199
  f"— the person being reviewed cannot supply the review")
174
200
  add("verdict is APPROVED", verdict["verdict"] == "APPROVED",
175
201
  f"verdict={verdict['verdict']}")
@@ -138,7 +138,21 @@ def _resolve_state_dir(arg: Optional[str], self_name_arg: Optional[str]) -> Path
138
138
 
139
139
 
140
140
  def _resolve_token(token_file_arg: Optional[str]) -> str:
141
- """Mirror onboard's resolution. env → secrets.toml mode 0600 → prompt."""
141
+ """DELEGATES to onboard's resolver (#243) — and this docstring used to lie.
142
+
143
+ It read "Mirror onboard's resolution. env -> secrets.toml mode 0600 ->
144
+ prompt", which describes a COPY. The body has always been a delegation, so
145
+ the fix for #243 reached this verb for free — but a reader auditing which
146
+ verbs inherited the retired-credential bug would have read this line and
147
+ concluded daemon had its own broken copy. I did exactly that, and carded it.
148
+
149
+ >>> A DOCSTRING THAT DESCRIBES A COPY OVER A BODY THAT DELEGATES IS A STALE
150
+ RECORD, AND IT COSTS THE NEXT READER THE SAME WRONG CONCLUSION. <<< The
151
+ resolution order it names is also now wrong: onboard gained
152
+ ~/.config/swarph/<SWARPH_SELF>.peer_token and REFUSES rather than prompting.
153
+ Naming the delegation instead of restating the order means this cannot go
154
+ stale again.
155
+ """
142
156
  from swarph_cli.commands.onboard import _resolve_token as _onboard_resolve
143
157
 
144
158
  return _onboard_resolve(token_file_arg)
@@ -167,14 +167,51 @@ def _resolve_token(token_file_arg: Optional[str]) -> str:
167
167
  f"swarph onboard: failed to read {secrets_path}: {exc}", file=sys.stderr
168
168
  )
169
169
 
170
- print_safe(
171
- f"swarph onboard: MESH_GATEWAY_TOKEN not in env, not found in {secrets_path}.\n"
172
- f" Canonical secrets.toml shape (mode 0600):\n"
173
- f" MESH_GATEWAY_TOKEN=<your-token>\n"
174
- f" Falling back to interactive prompt.",
175
- file=sys.stderr,
170
+ # ── #243: THE PER-PEER TOKEN — THE CREDENTIAL THAT ACTUALLY EXISTS ──────
171
+ # >>> THIS VERB WAS LOOKING ONLY FOR THE CREDENTIAL THE R1 MIGRATION
172
+ # RETIRED. <<< Measured on lab-ovh 2026-08-03: $MESH_GATEWAY_TOKEN UNSET,
173
+ # ~/.swarph/secrets.toml ABSENT, and 10+ files present at
174
+ # ~/.config/swarph/<peer>.peer_token. Every other verb resolves the per-peer
175
+ # file; onboard (and ratify, which re-exports this function, and daemon,
176
+ # which copied it) were left on the old path — classic mint-vs-cutover, the
177
+ # credential moved and three consumers did not.
178
+ #
179
+ # AND THE GATEWAY NEVER REQUIRED THE SHARED TOKEN: measured, POST
180
+ # /peers/register with a PER-PEER token returns 200 and mints. So this was
181
+ # never a permissions problem — only a lookup that never learned.
182
+ #
183
+ # Placed AFTER the existing two so a working operator-token setup is
184
+ # unchanged; this only fills the hole where the verb used to prompt.
185
+ self_name = os.environ.get("SWARPH_SELF", "").strip()
186
+ if self_name:
187
+ peer_tok = Path.home() / ".config" / "swarph" / f"{self_name}.peer_token"
188
+ if peer_tok.exists():
189
+ try:
190
+ val = peer_tok.read_text(encoding="utf-8").strip()
191
+ if val:
192
+ return val
193
+ except Exception as exc:
194
+ print_safe(f"swarph onboard: failed to read {peer_tok}: {exc}",
195
+ file=sys.stderr)
196
+
197
+ # >>> REFUSE. DO NOT PROMPT. <<< getpass on a non-tty is a guaranteed
198
+ # EOFError, and this verb runs from scripts, cron and spawned cells — the
199
+ # traceback it produced was reported as a BROKEN VERB, not as a missing
200
+ # credential, because the prompt hid which of the two it was.
201
+ #
202
+ # NOTE the deliberate absence of a default for SWARPH_SELF: guessing a name
203
+ # makes a cell hunt ANOTHER CELL'S token, find nothing, and blame the
204
+ # credential — measured on 6 of 6 cells 2026-07-29. Unset is named as unset.
205
+ raise RuntimeError(
206
+ "no gateway credential found. Tried, in order:\n"
207
+ f" 1. --token-file {'(not given)' if not token_file_arg else token_file_arg}\n"
208
+ " 2. $MESH_GATEWAY_TOKEN (unset)\n"
209
+ f" 3. {secrets_path}\n"
210
+ f" 4. ~/.config/swarph/<self>.peer_token "
211
+ + (f"(SWARPH_SELF={self_name!r} -> not found)" if self_name
212
+ else "(SWARPH_SELF IS UNSET — set it; this verb will not guess a peer name)")
213
+ + "\n The per-peer token is sufficient: POST /peers/register accepts it."
176
214
  )
177
- return getpass("MESH_GATEWAY_TOKEN: ").strip()
178
215
 
179
216
 
180
217
  def _post_json(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swarph-cli
3
- Version: 0.41.2
3
+ Version: 0.41.4
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`, session `import`, and a stranded-session `watchdog`.
5
5
  Author: Pierre Samson, Claude Opus
6
6
  License: MIT
@@ -94,6 +94,8 @@ src/swarph_cli/service/providers.py
94
94
  src/swarph_cli/systemd/swarph-watchdog.default
95
95
  src/swarph_cli/systemd/swarph-watchdog.service
96
96
  src/swarph_cli/systemd/swarph-watchdog.timer
97
+ tests/test_243_onboard_per_peer_token.py
98
+ tests/test_84_cli_labels.py
97
99
  tests/test_add_security_gate.py
98
100
  tests/test_artifact_add.py
99
101
  tests/test_artifact_hash.py
@@ -0,0 +1,86 @@
1
+ """#243 — `swarph onboard` resolved ONLY the credential the R1 migration retired.
2
+
3
+ Measured on lab-ovh 2026-08-03: $MESH_GATEWAY_TOKEN UNSET, ~/.swarph/secrets.toml
4
+ ABSENT, and 10+ files present at ~/.config/swarph/<peer>.peer_token. Every other
5
+ verb resolves the per-peer file; onboard (and ratify, which re-exports it, and
6
+ daemon, which COPIED it) were left on the old path.
7
+
8
+ The gateway never required the shared token — POST /peers/register accepts a
9
+ per-peer token and mints (measured). So this was never permissions, only a lookup
10
+ that never learned.
11
+ """
12
+ import pytest
13
+
14
+ from swarph_cli.commands import onboard
15
+
16
+
17
+ @pytest.fixture(autouse=True)
18
+ def _no_ambient_credentials(monkeypatch, tmp_path):
19
+ """The bug is invisible on a box that still has the old credential, so the
20
+ fixture removes BOTH retired sources — which is the state of every cell
21
+ since the migration."""
22
+ monkeypatch.delenv("MESH_GATEWAY_TOKEN", raising=False)
23
+ monkeypatch.delenv("SWARPH_SELF", raising=False)
24
+ monkeypatch.setattr(onboard.Path, "home", staticmethod(lambda: tmp_path))
25
+ return tmp_path
26
+
27
+
28
+ def _write_peer_token(home, peer, value="tok-per-peer"):
29
+ d = home / ".config" / "swarph"
30
+ d.mkdir(parents=True, exist_ok=True)
31
+ (d / f"{peer}.peer_token").write_text(value, encoding="utf-8")
32
+
33
+
34
+ def test_it_resolves_the_PER_PEER_token(_no_ambient_credentials, monkeypatch):
35
+ """>>> THE BUG. <<< With no shared token anywhere — the state of every cell
36
+ since R1 — onboard must find the per-peer file rather than prompting."""
37
+ _write_peer_token(_no_ambient_credentials, "lab-ovh")
38
+ monkeypatch.setenv("SWARPH_SELF", "lab-ovh")
39
+ assert onboard._resolve_token(None) == "tok-per-peer"
40
+
41
+
42
+ def test_it_REFUSES_instead_of_prompting_when_nothing_is_found(_no_ambient_credentials):
43
+ """getpass on a non-tty is a guaranteed EOFError, and this verb runs from
44
+ scripts, cron and spawned cells. The refusal must NAME the places it looked —
45
+ the prompt is what made a missing credential look like a broken verb."""
46
+ with pytest.raises(RuntimeError) as e:
47
+ onboard._resolve_token(None)
48
+ msg = str(e.value)
49
+ assert "MESH_GATEWAY_TOKEN" in msg
50
+ assert "peer_token" in msg
51
+ assert "SWARPH_SELF IS UNSET" in msg, (
52
+ "an unset SWARPH_SELF must be named as unset — guessing a peer name makes "
53
+ "a cell hunt ANOTHER CELL'S token and blame the credential")
54
+
55
+
56
+ def test_it_does_NOT_guess_a_peer_name(_no_ambient_credentials):
57
+ """MEASURED 2026-07-29 on 6 of 6 cells: a default self-name makes a cell look
58
+ for lab-ovh's token, find nothing, and report a TOKEN fault. Harmless on
59
+ lab-ovh, silently wrong everywhere else."""
60
+ _write_peer_token(_no_ambient_credentials, "lab-ovh") # present but not ours
61
+ with pytest.raises(RuntimeError):
62
+ onboard._resolve_token(None) # SWARPH_SELF unset
63
+
64
+
65
+ def test_the_SHARED_token_still_wins_when_present(_no_ambient_credentials, monkeypatch):
66
+ """NON-VACUITY. The per-peer path is ADDITIVE — an operator with a working
67
+ shared-token setup must be unaffected, or the fix trades one broken
68
+ population for another."""
69
+ _write_peer_token(_no_ambient_credentials, "lab-ovh", "tok-per-peer")
70
+ monkeypatch.setenv("SWARPH_SELF", "lab-ovh")
71
+ monkeypatch.setenv("MESH_GATEWAY_TOKEN", "tok-shared")
72
+ assert onboard._resolve_token(None) == "tok-shared"
73
+
74
+
75
+ def test_ratify_and_daemon_resolve_through_THE_SAME_function():
76
+ """>>> ratify RE-EXPORTED and was fixed for free; daemon COPIED and was not.
77
+ <<< A copy made to keep two things identical keeps them identical only until
78
+ one is fixed. Both now delegate, so the next credential migration cannot
79
+ break one and not the others."""
80
+ from swarph_cli.commands import ratify, daemon
81
+ import inspect
82
+ for mod in (ratify, daemon):
83
+ src = inspect.getsource(mod._resolve_token)
84
+ assert "from swarph_cli.commands.onboard import _resolve_token" in src, (
85
+ f"{mod.__name__} does not delegate — it has its own copy and will "
86
+ "diverge at the next migration")
@@ -0,0 +1,81 @@
1
+ """#84 CLI half — `--label` filter, `--label` on add, `cards label add/rm`.
2
+
3
+ The gateway takes a FULL REPLACEMENT for labels (they are a set — a merge-only
4
+ field could never REMOVE one), so `label rm` is unavoidably read-modify-write.
5
+ The risk that creates is the one these tests pin: a replacement built on a FAILED
6
+ read would silently drop every other label on the card.
7
+ """
8
+ import pytest
9
+
10
+ from swarph_cli.commands import board as b
11
+
12
+
13
+ # ── the pure transform, so the RMW is testable without a gateway ────────────
14
+
15
+ @pytest.mark.parametrize("cur,action,lab,want", [
16
+ ([], "add", "obligation-ledger", ["obligation-ledger"]),
17
+ (["a"], "add", "b", ["a", "b"]),
18
+ (["a", "b"], "rm", "a", ["b"]),
19
+ (["a"], "rm", "nope", ["a"]), # removing an absent label is a no-op
20
+ (["a"], "add", "a", ["a"]), # idempotent add
21
+ (["a"], "add", "A", ["a"]), # case is a WRITING variation, not a new label
22
+ (["A", "B"], "rm", "a", ["b"]), # ...on both sides
23
+ (None, "add", "x", ["x"]), # a card with no labels column yet
24
+ ])
25
+ def test_apply_label(cur, action, lab, want):
26
+ assert b._apply_label(cur, action, lab) == want
27
+
28
+
29
+ def test_add_is_idempotent_and_order_preserving():
30
+ """Labels are a SET but the ORDER is what a human reads. Re-adding must not
31
+ reorder — a card whose labels shuffle on every touch is noise in the diff."""
32
+ out = ["one", "two", "three"]
33
+ for lab in ("one", "two", "three"):
34
+ out = b._apply_label(out, "add", lab)
35
+ assert out == ["one", "two", "three"]
36
+
37
+
38
+ # ── the URL + payload builders ─────────────────────────────────────────────
39
+
40
+ def test_list_url_carries_the_label_filter():
41
+ url = b._cards_list_url("http://gw", project=8, label="obligation-ledger")
42
+ assert "label=obligation-ledger" in url
43
+ assert "project=8" in url
44
+
45
+
46
+ def test_list_url_omits_the_filter_when_absent():
47
+ """NON-VACUITY: an empty filter must not become `?label=`, which the gateway
48
+ would read as a request for cards labelled with the empty string."""
49
+ assert "label" not in b._cards_list_url("http://gw", project=8)
50
+ assert "label" not in b._cards_list_url("http://gw", project=8, label=None)
51
+ assert "label" not in b._cards_list_url("http://gw", project=8, label="")
52
+
53
+
54
+ def test_add_payload_carries_labels_only_when_given():
55
+ assert "labels" not in b._card_add_payload("me", 8, "t")
56
+ p = b._card_add_payload("me", 8, "t", labels=["a", "b"])
57
+ assert p["labels"] == ["a", "b"]
58
+
59
+
60
+ # ── >>> THE CLOBBER GUARD — the reason the RMW is safe <<< ──────────────────
61
+
62
+ def test_label_REFUSES_to_write_when_the_read_FAILS(monkeypatch, capsys):
63
+ """>>> A REPLACEMENT BUILT ON A FAILED READ WOULD DROP EVERY OTHER LABEL. <<<
64
+
65
+ `label add` must GET the card, compute the new set, then PATCH the whole
66
+ array. If the GET fails and the code proceeds with `[]` as "current", the
67
+ PATCH silently erases the card's other labels — a data-loss bug that looks
68
+ like a successful write.
69
+
70
+ Pinned by making the GET fail and asserting NOTHING is patched.
71
+ """
72
+ patched = []
73
+ monkeypatch.setattr(b, "_http_get_json", lambda *a, **k: (503, {"detail": "down"}))
74
+ monkeypatch.setattr(b, "_patch_json", lambda *a, **k: patched.append(a) or (200, {}))
75
+ monkeypatch.setattr(b, "_resolve_self_name", lambda *_: "lab-ovh")
76
+ monkeypatch.setattr(b, "_resolve_token", lambda *_: "tok")
77
+
78
+ rc = b.run_board(["cards", "label", "add", "1", "x", "--gateway", "http://gw"])
79
+ assert rc == 1, "a failed read must refuse, not fall through to a write"
80
+ assert patched == [], "PATCHED after a failed GET — the other labels were clobbered"
81
+ assert "refusing to write" in capsys.readouterr().err
@@ -347,3 +347,50 @@ def test_a_READY_card_with_NO_peer_verdict_REFUSES():
347
347
  d = mc.decide(_card(links={"pr": "https://github.com/o/r/pull/152"}), _pr())
348
348
  assert _fail(d, "peer verdict recorded on the card")
349
349
  assert d.verdict != "WOULD_MERGE"
350
+
351
+
352
+ def test_a_stamp_WITHOUT_the_self_authored_KEY_abstains_like_no_stamp():
353
+ """>>> THE BUG THAT SHIPPED IN 0.41.2, AND IT WAS 92% OF PRODUCTION. <<<
354
+
355
+ Found by droplet RUNNING decide() on the installed package instead of reading
356
+ it. The first version tested `_vstamp is None` — absence of the STAMP. The
357
+ backlog's real shape is a stamp that EXISTS (written before self_authored
358
+ shipped) and lacks the KEY: it fell through to the comparison, where
359
+ `.get("self_authored")` returns None, `not None` is True, and A TEXTBOOK
360
+ SELF-REVIEW PASSED.
361
+
362
+ Measured at the time: 53 link_stamps in production, 4 carrying the flag — 49
363
+ (92%) were this shape. The leg was VACUOUS FOR EXACTLY THE BACKLOG IT WAS
364
+ BUILT FOR, which is the outcome the design explicitly targets and which I had
365
+ reported to the date-holder as avoided. He authorised near-total ABSTENTION;
366
+ what shipped was near-total PASS.
367
+
368
+ WHY THE VERIFICATION MISSED IT: the wheel audit and the end-to-end check
369
+ covered self_authored=True (refuses) and no-stamp (abstains). Both correct,
370
+ NEITHER IS THE PRODUCTION SHAPE. The untested case was the only one that
371
+ exists in the data.
372
+ """
373
+ stamp_no_key = {"peer_verdict": {"by": "droplet", "caller_bound": True,
374
+ "at": "2026-08-01T00:00:00Z"}}
375
+ d = mc.decide(_card(created_by="lab-ovh", assignee="droplet",
376
+ link_stamps=stamp_no_key), _pr())
377
+ check = [c for c in d.checks if c.name == "verdict is not self-authored"][0]
378
+ assert check.ok is None, (
379
+ "a stamp lacking the self_authored KEY passed the self-review leg — "
380
+ "vacuous for 92% of production")
381
+ assert d.verdict != "WOULD_MERGE"
382
+
383
+
384
+ def test_the_two_BLIND_causes_are_named_distinctly():
385
+ """They decay differently and must not print the same: a MISSING STAMP is a
386
+ producer that dropped it (outage shape); a MISSING KEY is a verdict older than
387
+ the flag (drains to zero as verdicts are rewritten). One message for both
388
+ would make a permanent outage indistinguishable from a shrinking backlog."""
389
+ no_stamp = mc.decide(_card(created_by="lab-ovh", link_stamps={}), _pr())
390
+ no_key = mc.decide(_card(created_by="lab-ovh",
391
+ link_stamps={"peer_verdict": {"by": "droplet"}}), _pr())
392
+ d1 = [c for c in no_stamp.checks if c.name == "verdict is not self-authored"][0].detail
393
+ d2 = [c for c in no_key.checks if c.name == "verdict is not self-authored"][0].detail
394
+ assert d1 != d2, "both blind causes printed the same detail"
395
+ assert "at all" in d1
396
+ assert "predates the flag" in d2
@@ -17,7 +17,7 @@ from swarph_cli.commands import brain_ask as ba
17
17
 
18
18
  def test_version_is_0_33_0():
19
19
  import swarph_cli
20
- assert swarph_cli.__version__ == "0.41.2"
20
+ assert swarph_cli.__version__ == "0.41.4"
21
21
 
22
22
 
23
23
  # --- request / response plumbing -------------------------------------------
@@ -48,14 +48,30 @@ def test_resolve_token_warns_on_loose_mode(monkeypatch, tmp_path, capsys):
48
48
  assert "0o644" in err
49
49
 
50
50
 
51
- def test_resolve_token_falls_back_to_prompt(monkeypatch, tmp_path, capsys):
51
+ def test_resolve_token_REFUSES_rather_than_prompting(monkeypatch, tmp_path):
52
+ """>>> THIS TEST USED TO ASSERT THE PROMPT, AND THE PROMPT WAS THE BUG (#243).
53
+ <<<
54
+
55
+ It pinned `falls_back_to_prompt` — correct when a shared MESH_GATEWAY_TOKEN
56
+ was the only credential. After the R1 per-peer migration neither
57
+ $MESH_GATEWAY_TOKEN nor ~/.swarph/secrets.toml exists on any cell, so the
58
+ prompt became the DEFAULT path — and getpass on a non-tty is a guaranteed
59
+ EOFError. `swarph onboard` was reported as a broken verb; it was a missing
60
+ credential wearing a traceback.
61
+
62
+ The expectation is flipped DELIBERATELY: refuse, naming every place it
63
+ looked, so the operator learns which credential is absent instead of meeting
64
+ a prompt that cannot be answered.
65
+ """
52
66
  monkeypatch.delenv("MESH_GATEWAY_TOKEN", raising=False)
53
- monkeypatch.setattr(onboard, "getpass", lambda _: "prompted-tok")
54
- fake = tmp_path / "no-such-file.toml"
55
- out = onboard._resolve_token(str(fake))
56
- assert out == "prompted-tok"
57
- err = capsys.readouterr().err
58
- assert "secrets.toml shape" in err # operator learns the pattern
67
+ monkeypatch.delenv("SWARPH_SELF", raising=False)
68
+ monkeypatch.setattr(onboard.Path, "home", staticmethod(lambda: tmp_path))
69
+ with pytest.raises(RuntimeError) as e:
70
+ onboard._resolve_token(str(tmp_path / "no-such-file.toml"))
71
+ msg = str(e.value)
72
+ assert "MESH_GATEWAY_TOKEN" in msg and "peer_token" in msg, (
73
+ "the refusal must name every place it looked — a bare failure leaves the "
74
+ "operator guessing which of four credentials is missing")
59
75
 
60
76
 
61
77
  # ---------------------------------------------------------------------------
@@ -1043,4 +1043,4 @@ def test_parser_defaults_process_name_claude_liveness_cmd_none():
1043
1043
 
1044
1044
  def test_version_is_0_33_0():
1045
1045
  import swarph_cli
1046
- assert swarph_cli.__version__ == "0.41.2"
1046
+ assert swarph_cli.__version__ == "0.41.4"
File without changes
File without changes
File without changes