swarph-cli 0.41.3__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.3/src/swarph_cli.egg-info → swarph_cli-0.41.4}/PKG-INFO +1 -1
  2. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/pyproject.toml +1 -1
  3. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/__init__.py +1 -1
  4. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/board.py +57 -6
  5. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/daemon.py +15 -1
  6. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/onboard.py +44 -7
  7. {swarph_cli-0.41.3 → swarph_cli-0.41.4/src/swarph_cli.egg-info}/PKG-INFO +1 -1
  8. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli.egg-info/SOURCES.txt +2 -0
  9. swarph_cli-0.41.4/tests/test_243_onboard_per_peer_token.py +86 -0
  10. swarph_cli-0.41.4/tests/test_84_cli_labels.py +81 -0
  11. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_brain_ask_command.py +1 -1
  12. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_onboard_command.py +23 -7
  13. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_watchdog.py +1 -1
  14. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/LICENSE +0 -0
  15. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/README.md +0 -0
  16. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/setup.cfg +0 -0
  17. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/bench/__init__.py +0 -0
  18. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/bench/backends.py +0 -0
  19. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/bench/pack.py +0 -0
  20. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/bench/prices.py +0 -0
  21. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/bench/quality.py +0 -0
  22. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/bench/refresh_prices.py +0 -0
  23. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/bench/runner.py +0 -0
  24. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/bench/validate.py +0 -0
  25. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/caller.py +0 -0
  26. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/capture/__init__.py +0 -0
  27. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/capture/harden.py +0 -0
  28. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/capture/lineage.py +0 -0
  29. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/capture/liveness.py +0 -0
  30. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/capture/manifest.py +0 -0
  31. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/capture/paths.py +0 -0
  32. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/capture/verify.py +0 -0
  33. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/cell.py +0 -0
  34. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/chain_token.py +0 -0
  35. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/__init__.py +0 -0
  36. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/_display.py +0 -0
  37. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/_gateway_client.py +0 -0
  38. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/add.py +0 -0
  39. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/bench.py +0 -0
  40. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/board_merge_check.py +0 -0
  41. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/brain.py +0 -0
  42. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/brain_ask.py +0 -0
  43. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/cell.py +0 -0
  44. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/cell_selfcheck.py +0 -0
  45. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/channel.py +0 -0
  46. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/chat.py +0 -0
  47. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/codegraph.py +0 -0
  48. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/codegraph_hook.py +0 -0
  49. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/compress.py +0 -0
  50. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/event.py +0 -0
  51. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/gateway.py +0 -0
  52. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/group.py +0 -0
  53. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/highlight.py +0 -0
  54. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/hook_output.py +0 -0
  55. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/hooks.py +0 -0
  56. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/import_session.py +0 -0
  57. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/init.py +0 -0
  58. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/install_hook.py +0 -0
  59. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/install_multiplexer.py +0 -0
  60. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/lane.py +0 -0
  61. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/mcp_server.py +0 -0
  62. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/memory.py +0 -0
  63. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/memory_sync.py +0 -0
  64. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/mesh.py +0 -0
  65. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/monitor.py +0 -0
  66. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/okf_links.py +0 -0
  67. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/protocol_handler.py +0 -0
  68. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/ratify.py +0 -0
  69. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/schedule.py +0 -0
  70. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/security.py +0 -0
  71. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/service.py +0 -0
  72. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/spawn.py +0 -0
  73. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/timeline.py +0 -0
  74. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/commands/watchdog.py +0 -0
  75. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/compress/__init__.py +0 -0
  76. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/compress/levers.py +0 -0
  77. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/compress/marker.py +0 -0
  78. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/compress/verify.py +0 -0
  79. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/console_safe.py +0 -0
  80. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/delivery_queue.py +0 -0
  81. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/gateway/__init__.py +0 -0
  82. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/gateway/feature_registry.py +0 -0
  83. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/gateway/lanes_control.py +0 -0
  84. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/gateway/schema.sql +0 -0
  85. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/gateway/server.py +0 -0
  86. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/gateway/services_control.py +0 -0
  87. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/main.py +0 -0
  88. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/multiplexer.py +0 -0
  89. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/parsers/__init__.py +0 -0
  90. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/parsers/claude.py +0 -0
  91. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/scripts/ensure_monitor.sh +0 -0
  92. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/service/__init__.py +0 -0
  93. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/service/app.py +0 -0
  94. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/service/providers.py +0 -0
  95. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/session_bridge.py +0 -0
  96. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/stall_alert.py +0 -0
  97. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
  98. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
  99. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
  100. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
  101. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli.egg-info/entry_points.txt +0 -0
  102. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli.egg-info/requires.txt +0 -0
  103. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/src/swarph_cli.egg-info/top_level.txt +0 -0
  104. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_add_security_gate.py +0 -0
  105. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_artifact_add.py +0 -0
  106. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_artifact_hash.py +0 -0
  107. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_artifact_lib.py +0 -0
  108. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_artifact_mcp.py +0 -0
  109. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_artifact_skill.py +0 -0
  110. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_artifact_tool.py +0 -0
  111. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_artifact_uri.py +0 -0
  112. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_bench_backends.py +0 -0
  113. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_bench_command.py +0 -0
  114. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_bench_pack.py +0 -0
  115. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_bench_prices.py +0 -0
  116. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_bench_quality.py +0 -0
  117. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_bench_runner.py +0 -0
  118. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_bench_validate.py +0 -0
  119. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_board_command.py +0 -0
  120. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_board_merge_check.py +0 -0
  121. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_brain_ask_env_diagnosis.py +0 -0
  122. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_brain_command.py +0 -0
  123. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_caller_meta_guard.py +0 -0
  124. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_capture_lineage.py +0 -0
  125. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_capture_liveness.py +0 -0
  126. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_capture_manifest.py +0 -0
  127. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_capture_paths.py +0 -0
  128. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_capture_security.py +0 -0
  129. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_cell_command_dispatch.py +0 -0
  130. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_cell_harden.py +0 -0
  131. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_cell_loader.py +0 -0
  132. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_cell_selfcheck.py +0 -0
  133. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_cell_verify.py +0 -0
  134. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_chain_token.py +0 -0
  135. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_channel_command.py +0 -0
  136. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_channel_post.py +0 -0
  137. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_chat_command.py +0 -0
  138. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_claude_parser.py +0 -0
  139. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_claude_tmux_template.py +0 -0
  140. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_codegraph_caller_scope.py +0 -0
  141. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_codegraph_command.py +0 -0
  142. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_codegraph_hook.py +0 -0
  143. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_codegraph_mcp.py +0 -0
  144. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_compress_caller_convention.py +0 -0
  145. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_compress_command.py +0 -0
  146. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_compress_levers.py +0 -0
  147. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_compress_marker.py +0 -0
  148. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_compress_verify.py +0 -0
  149. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_console_encoding_e2e.py +0 -0
  150. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_daemon_command.py +0 -0
  151. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_daemon_console_encoding.py +0 -0
  152. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_daemon_delivery.py +0 -0
  153. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_delivery_queue.py +0 -0
  154. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_display_sanitize.py +0 -0
  155. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_event_emit.py +0 -0
  156. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_feature_to_uri.py +0 -0
  157. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_gateway_brain.py +0 -0
  158. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_gateway_client.py +0 -0
  159. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_gateway_command.py +0 -0
  160. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_group_command.py +0 -0
  161. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_highlight_command.py +0 -0
  162. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_hook_output.py +0 -0
  163. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_hooks_activity_marker.py +0 -0
  164. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_hooks_add.py +0 -0
  165. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_hooks_bundle.py +0 -0
  166. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_hooks_init.py +0 -0
  167. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_hooks_lifecycle.py +0 -0
  168. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_hooks_merge.py +0 -0
  169. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_import_command.py +0 -0
  170. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_init_command.py +0 -0
  171. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_install_hook.py +0 -0
  172. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_install_multiplexer_command.py +0 -0
  173. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_lane_command.py +0 -0
  174. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_main.py +0 -0
  175. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_main_unknown_verb_guard.py +0 -0
  176. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_mcp_server.py +0 -0
  177. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_memory_command.py +0 -0
  178. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_memory_gateway.py +0 -0
  179. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_memory_sync.py +0 -0
  180. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_memory_sync_membrane.py +0 -0
  181. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_mesh_command.py +0 -0
  182. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_mesh_inbox_mark_read.py +0 -0
  183. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_mesh_inbox_sanitize.py +0 -0
  184. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_mesh_sidecar.py +0 -0
  185. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_mesh_sidecar_cursor_decoupled.py +0 -0
  186. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_mesh_sidecar_wake_survives_read.py +0 -0
  187. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_meta_edge_identity.py +0 -0
  188. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_monitor_cli.py +0 -0
  189. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_monitor_daemon_collision.py +0 -0
  190. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_monitor_self_name_verification.py +0 -0
  191. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_monitor_state_split.py +0 -0
  192. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_multiplexer.py +0 -0
  193. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_okf_links.py +0 -0
  194. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_packaged_artifacts_exist.py +0 -0
  195. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_process_liveness.py +0 -0
  196. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_protocol_handler.py +0 -0
  197. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_providers_isolation.py +0 -0
  198. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_ratify_command.py +0 -0
  199. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_repl_caller_convention.py +0 -0
  200. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_schedule_command.py +0 -0
  201. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_schedule_create_contract.py +0 -0
  202. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_security.py +0 -0
  203. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_service_app.py +0 -0
  204. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_service_command.py +0 -0
  205. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_service_providers.py +0 -0
  206. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_session_bridge_inject.py +0 -0
  207. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_session_bridge_probe.py +0 -0
  208. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_session_bridge_resolve.py +0 -0
  209. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_session_path_role_gate.py +0 -0
  210. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_shipped_units_match_the_cli.py +0 -0
  211. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_smoke_chat.py +0 -0
  212. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_smoke_one_shot.py +0 -0
  213. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_smoke_phase_5_5.py +0 -0
  214. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_spawn_base_hoist.py +0 -0
  215. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_spawn_command.py +0 -0
  216. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_spawn_live_pin.py +0 -0
  217. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_spawn_mitosis_lineage.py +0 -0
  218. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_spawn_tmux_session.py +0 -0
  219. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_spawn_windows_relaunch.py +0 -0
  220. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_stall_alert.py +0 -0
  221. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_timeline_command.py +0 -0
  222. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_token_file_one_parser.py +0 -0
  223. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_version_consistency.py +0 -0
  224. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_watchdog_dm_wake.py +0 -0
  225. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
  226. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_watchdog_dm_wake_wiring.py +0 -0
  227. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_watchdog_emit_health.py +0 -0
  228. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_watchdog_install_flags.py +0 -0
  229. {swarph_cli-0.41.3 → swarph_cli-0.41.4}/tests/test_watchdog_model_rung.py +0 -0
  230. {swarph_cli-0.41.3 → 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.3
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.3"
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.3"
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)
@@ -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.3
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
@@ -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.3"
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.3"
1046
+ assert swarph_cli.__version__ == "0.41.4"
File without changes
File without changes
File without changes