swarph-cli 0.40.2__tar.gz → 0.41.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. {swarph_cli-0.40.2/src/swarph_cli.egg-info → swarph_cli-0.41.0}/PKG-INFO +1 -1
  2. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/pyproject.toml +1 -1
  3. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/__init__.py +1 -1
  4. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/codegraph.py +20 -1
  5. swarph_cli-0.41.0/src/swarph_cli/commands/codegraph_hook.py +233 -0
  6. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/hooks.py +36 -0
  7. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/mesh.py +82 -5
  8. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/schedule.py +75 -2
  9. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/gateway/server.py +9 -2
  10. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/main.py +1 -0
  11. {swarph_cli-0.40.2 → swarph_cli-0.41.0/src/swarph_cli.egg-info}/PKG-INFO +1 -1
  12. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli.egg-info/SOURCES.txt +5 -0
  13. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_brain_ask_command.py +1 -1
  14. swarph_cli-0.41.0/tests/test_codegraph_caller_scope.py +103 -0
  15. swarph_cli-0.41.0/tests/test_codegraph_hook.py +145 -0
  16. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_meta_edge_identity.py +47 -0
  17. swarph_cli-0.41.0/tests/test_process_liveness.py +97 -0
  18. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_schedule_command.py +35 -4
  19. swarph_cli-0.41.0/tests/test_schedule_create_contract.py +154 -0
  20. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_watchdog.py +1 -1
  21. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/LICENSE +0 -0
  22. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/README.md +0 -0
  23. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/setup.cfg +0 -0
  24. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/bench/__init__.py +0 -0
  25. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/bench/backends.py +0 -0
  26. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/bench/pack.py +0 -0
  27. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/bench/prices.py +0 -0
  28. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/bench/quality.py +0 -0
  29. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/bench/refresh_prices.py +0 -0
  30. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/bench/runner.py +0 -0
  31. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/bench/validate.py +0 -0
  32. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/caller.py +0 -0
  33. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/capture/__init__.py +0 -0
  34. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/capture/harden.py +0 -0
  35. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/capture/lineage.py +0 -0
  36. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/capture/liveness.py +0 -0
  37. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/capture/manifest.py +0 -0
  38. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/capture/paths.py +0 -0
  39. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/capture/verify.py +0 -0
  40. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/cell.py +0 -0
  41. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/chain_token.py +0 -0
  42. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/__init__.py +0 -0
  43. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/_display.py +0 -0
  44. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/_gateway_client.py +0 -0
  45. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/add.py +0 -0
  46. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/bench.py +0 -0
  47. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/board.py +0 -0
  48. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/board_merge_check.py +0 -0
  49. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/brain.py +0 -0
  50. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/brain_ask.py +0 -0
  51. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/cell.py +0 -0
  52. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/cell_selfcheck.py +0 -0
  53. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/channel.py +0 -0
  54. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/chat.py +0 -0
  55. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/compress.py +0 -0
  56. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/daemon.py +0 -0
  57. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/event.py +0 -0
  58. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/gateway.py +0 -0
  59. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/group.py +0 -0
  60. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/highlight.py +0 -0
  61. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/hook_output.py +0 -0
  62. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/import_session.py +0 -0
  63. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/init.py +0 -0
  64. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/install_hook.py +0 -0
  65. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/install_multiplexer.py +0 -0
  66. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/lane.py +0 -0
  67. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/mcp_server.py +0 -0
  68. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/memory.py +0 -0
  69. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/memory_sync.py +0 -0
  70. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/monitor.py +0 -0
  71. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/okf_links.py +0 -0
  72. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/onboard.py +0 -0
  73. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/protocol_handler.py +0 -0
  74. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/ratify.py +0 -0
  75. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/security.py +0 -0
  76. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/service.py +0 -0
  77. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/spawn.py +0 -0
  78. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/timeline.py +0 -0
  79. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/commands/watchdog.py +0 -0
  80. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/compress/__init__.py +0 -0
  81. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/compress/levers.py +0 -0
  82. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/compress/marker.py +0 -0
  83. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/compress/verify.py +0 -0
  84. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/console_safe.py +0 -0
  85. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/delivery_queue.py +0 -0
  86. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/gateway/__init__.py +0 -0
  87. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/gateway/feature_registry.py +0 -0
  88. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/gateway/lanes_control.py +0 -0
  89. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/gateway/schema.sql +0 -0
  90. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/gateway/services_control.py +0 -0
  91. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/multiplexer.py +0 -0
  92. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/parsers/__init__.py +0 -0
  93. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/parsers/claude.py +0 -0
  94. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/scripts/ensure_monitor.sh +0 -0
  95. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/service/__init__.py +0 -0
  96. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/service/app.py +0 -0
  97. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/service/providers.py +0 -0
  98. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/session_bridge.py +0 -0
  99. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/stall_alert.py +0 -0
  100. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
  101. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
  102. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
  103. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
  104. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli.egg-info/entry_points.txt +0 -0
  105. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli.egg-info/requires.txt +0 -0
  106. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/src/swarph_cli.egg-info/top_level.txt +0 -0
  107. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_add_security_gate.py +0 -0
  108. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_artifact_add.py +0 -0
  109. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_artifact_hash.py +0 -0
  110. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_artifact_lib.py +0 -0
  111. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_artifact_mcp.py +0 -0
  112. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_artifact_skill.py +0 -0
  113. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_artifact_tool.py +0 -0
  114. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_artifact_uri.py +0 -0
  115. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_bench_backends.py +0 -0
  116. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_bench_command.py +0 -0
  117. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_bench_pack.py +0 -0
  118. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_bench_prices.py +0 -0
  119. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_bench_quality.py +0 -0
  120. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_bench_runner.py +0 -0
  121. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_bench_validate.py +0 -0
  122. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_board_command.py +0 -0
  123. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_board_merge_check.py +0 -0
  124. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_brain_ask_env_diagnosis.py +0 -0
  125. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_brain_command.py +0 -0
  126. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_caller_meta_guard.py +0 -0
  127. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_capture_lineage.py +0 -0
  128. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_capture_liveness.py +0 -0
  129. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_capture_manifest.py +0 -0
  130. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_capture_paths.py +0 -0
  131. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_capture_security.py +0 -0
  132. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_cell_command_dispatch.py +0 -0
  133. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_cell_harden.py +0 -0
  134. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_cell_loader.py +0 -0
  135. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_cell_selfcheck.py +0 -0
  136. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_cell_verify.py +0 -0
  137. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_chain_token.py +0 -0
  138. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_channel_command.py +0 -0
  139. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_channel_post.py +0 -0
  140. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_chat_command.py +0 -0
  141. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_claude_parser.py +0 -0
  142. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_claude_tmux_template.py +0 -0
  143. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_codegraph_command.py +0 -0
  144. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_codegraph_mcp.py +0 -0
  145. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_compress_caller_convention.py +0 -0
  146. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_compress_command.py +0 -0
  147. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_compress_levers.py +0 -0
  148. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_compress_marker.py +0 -0
  149. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_compress_verify.py +0 -0
  150. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_console_encoding_e2e.py +0 -0
  151. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_daemon_command.py +0 -0
  152. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_daemon_console_encoding.py +0 -0
  153. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_daemon_delivery.py +0 -0
  154. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_delivery_queue.py +0 -0
  155. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_display_sanitize.py +0 -0
  156. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_event_emit.py +0 -0
  157. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_feature_to_uri.py +0 -0
  158. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_gateway_brain.py +0 -0
  159. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_gateway_client.py +0 -0
  160. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_gateway_command.py +0 -0
  161. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_group_command.py +0 -0
  162. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_highlight_command.py +0 -0
  163. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_hook_output.py +0 -0
  164. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_hooks_activity_marker.py +0 -0
  165. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_hooks_add.py +0 -0
  166. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_hooks_bundle.py +0 -0
  167. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_hooks_init.py +0 -0
  168. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_hooks_lifecycle.py +0 -0
  169. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_hooks_merge.py +0 -0
  170. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_import_command.py +0 -0
  171. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_init_command.py +0 -0
  172. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_install_hook.py +0 -0
  173. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_install_multiplexer_command.py +0 -0
  174. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_lane_command.py +0 -0
  175. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_main.py +0 -0
  176. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_main_unknown_verb_guard.py +0 -0
  177. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_mcp_server.py +0 -0
  178. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_memory_command.py +0 -0
  179. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_memory_gateway.py +0 -0
  180. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_memory_sync.py +0 -0
  181. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_memory_sync_membrane.py +0 -0
  182. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_mesh_command.py +0 -0
  183. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_mesh_inbox_mark_read.py +0 -0
  184. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_mesh_inbox_sanitize.py +0 -0
  185. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_mesh_sidecar.py +0 -0
  186. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_mesh_sidecar_cursor_decoupled.py +0 -0
  187. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_mesh_sidecar_wake_survives_read.py +0 -0
  188. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_monitor_cli.py +0 -0
  189. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_monitor_daemon_collision.py +0 -0
  190. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_monitor_self_name_verification.py +0 -0
  191. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_monitor_state_split.py +0 -0
  192. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_multiplexer.py +0 -0
  193. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_okf_links.py +0 -0
  194. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_onboard_command.py +0 -0
  195. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_packaged_artifacts_exist.py +0 -0
  196. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_protocol_handler.py +0 -0
  197. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_providers_isolation.py +0 -0
  198. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_ratify_command.py +0 -0
  199. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_repl_caller_convention.py +0 -0
  200. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_security.py +0 -0
  201. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_service_app.py +0 -0
  202. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_service_command.py +0 -0
  203. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_service_providers.py +0 -0
  204. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_session_bridge_inject.py +0 -0
  205. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_session_bridge_probe.py +0 -0
  206. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_session_bridge_resolve.py +0 -0
  207. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_session_path_role_gate.py +0 -0
  208. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_shipped_units_match_the_cli.py +0 -0
  209. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_smoke_chat.py +0 -0
  210. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_smoke_one_shot.py +0 -0
  211. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_smoke_phase_5_5.py +0 -0
  212. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_spawn_base_hoist.py +0 -0
  213. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_spawn_command.py +0 -0
  214. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_spawn_live_pin.py +0 -0
  215. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_spawn_mitosis_lineage.py +0 -0
  216. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_spawn_tmux_session.py +0 -0
  217. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_spawn_windows_relaunch.py +0 -0
  218. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_stall_alert.py +0 -0
  219. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_timeline_command.py +0 -0
  220. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_token_file_one_parser.py +0 -0
  221. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_version_consistency.py +0 -0
  222. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_watchdog_dm_wake.py +0 -0
  223. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
  224. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_watchdog_dm_wake_wiring.py +0 -0
  225. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_watchdog_emit_health.py +0 -0
  226. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_watchdog_install_flags.py +0 -0
  227. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/tests/test_watchdog_model_rung.py +0 -0
  228. {swarph_cli-0.40.2 → swarph_cli-0.41.0}/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.40.2
3
+ Version: 0.41.0
4
4
  Summary: The `swarph` binary — a multi-LLM CLI and mesh-gateway client + bundled server (`swarph gateway`): one-shot prompts across providers, interactive `chat`, multi-provider `spawn` (claude/codex/antigravity via a ProviderMembrane with subprocess billing-scrub), `mesh` send/inbox/register, `brain-ask` over the swarph-brain (gbrain) semantic memory, git-backed `assisted_memory`, 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.40.2"
7
+ version = "0.41.0"
8
8
  description = "The `swarph` binary — a multi-LLM CLI and mesh-gateway client + bundled server (`swarph gateway`): one-shot prompts across providers, interactive `chat`, multi-provider `spawn` (claude/codex/antigravity via a ProviderMembrane with subprocess billing-scrub), `mesh` send/inbox/register, `brain-ask` over the swarph-brain (gbrain) semantic memory, git-backed `assisted_memory`, 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.40.2"
19
+ __version__ = "0.41.0"
20
20
 
21
21
  __all__ = ["__version__"]
@@ -145,8 +145,27 @@ def structural_query(term, *, index_path, caller_cell, limit=8, allowlist=None)
145
145
  # verbatim edge kinds (contains/imports/decorates/references/calls/...), and counting
146
146
  # all of them would pollute the blast-radius number. 'calls' (plural) is CodeGraph's
147
147
  # actual call-edge kind.
148
+ #
149
+ # >>> CARD #194 — THE COUNT IS SCOPED TO THE CALLER-VISIBLE REPOS, exactly
150
+ # like the rows above. <<< This was an unscoped COUNT(*) while `s.repo IN
151
+ # (allowed)` gated the rows, so a caller who could NOT see a private repo
152
+ # still received a caller count that INCLUDED its callers.
153
+ #
154
+ # drop-on-meta-edge, seat-A: "a count that sums callers living in a private
155
+ # repo LEAKS THE EXISTENCE AND SIZE of those private callers — the exact
156
+ # payload #44's transport refuses, delivered as an integer instead of a
157
+ # path." Measured 2026-07-31: cross-visibility `calls` edges were 0 on the
158
+ # live index, so the leak was LATENT — but this is the blast-radius number
159
+ # the feature exists to serve, and the gateway proxy would have exposed it
160
+ # to every remote peer.
161
+ #
162
+ # The unifying rule: EVERY OBSERVABLE — rows, counts, scores, freshness —
163
+ # must be a function of only the caller-visible subset, never the full
164
+ # index. Gating the rows is ONE INSTANCE of it, not the whole of it.
148
165
  callers = con.execute(
149
- "SELECT COUNT(*) FROM edges WHERE dst_symbol=? AND edge_type='calls'", (sid,)
166
+ f"SELECT COUNT(*) FROM edges e JOIN symbols src ON src.id = e.src_symbol "
167
+ f"WHERE e.dst_symbol=? AND e.edge_type='calls' AND src.repo IN ({qmarks})",
168
+ (sid, *sorted(allowed))
150
169
  ).fetchone()[0]
151
170
  out.append({"repo": repo, "name": name, "kind": kind,
152
171
  "file_path": fp, "start_line": line, "callers": callers, "score": score,
@@ -0,0 +1,233 @@
1
+ """``swarph codegraph-hook`` — PostToolUse(Bash) structural-search companion.
2
+
3
+ WHEN A grep/rg SEARCHES CODE, ALSO ASK THE CODEGRAPH, and hand the structural
4
+ answer back as context. Card #194.
5
+
6
+ WHY IT EXISTS. Agents reach for grep where the symbol graph is the right
7
+ instrument — lab-ovh was corrected three times in one session for exactly that,
8
+ and the third correction landed INSIDE the fix for the first two. Instructions
9
+ demonstrably do not hold at the moment of typing, so this is mechanical rather
10
+ than a reminder. The token argument is the commander's: a grep that returns 200
11
+ matching lines costs far more context than the six symbols that actually answer
12
+ "where is this defined and who calls it".
13
+
14
+ >>> IT SUPPLEMENTS, IT NEVER BLOCKS. <<< grep is genuinely correct for config,
15
+ logs, /etc and string literals; the codegraph indexes SYMBOLS. The failure this
16
+ guards is "I only ever saw grep's answer", not "grep was used".
17
+
18
+ >>> AND IT MUST NEVER DEGRADE SILENTLY. <<< A missing index makes a structural
19
+ query return `[]` — indistinguishable from a real negative, which is the exact
20
+ shape of the (gbrain unreachable) incident and of card #200's drain. So an
21
+ unavailable backend is reported LOUDLY as an incident, never as "no matches".
22
+
23
+ PEER MODE. Unlike lab-ovh's original local hook, a peer has no local index — it
24
+ asks the gateway's ``POST /codegraph`` proxy, which applies the A8 visibility
25
+ gate to rows AND to caller counts (swarph-cli PR #165). The caller identity comes
26
+ from the bearer token, never from a field, so it cannot be self-asserted.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import json
32
+ import os
33
+ import re
34
+ import sys
35
+ import urllib.error
36
+ import urllib.request
37
+ from pathlib import Path
38
+ from typing import Optional
39
+
40
+ DEFAULT_GATEWAY = os.environ.get("MESH_GATEWAY_URL", "http://localhost:8788")
41
+ TIMEOUT_S = 6
42
+ MAX_ROWS = 6
43
+
44
+ # ── trigger heuristics ────────────────────────────────────────────────────
45
+ # Ported from the hook that ran on lab-ovh for a day. Every one of these three
46
+ # patterns exists because a LOOSER version misfired in production.
47
+
48
+ # 1. grep/rg must be at COMMAND POSITION — line start, or right after a pipe /
49
+ # ; / && / $( . Matching "grep" after arbitrary whitespace fires on any command
50
+ # whose text merely MENTIONS the word, including a heredoc body: the first live
51
+ # firing did exactly that and extracted the heredoc delimiter as the search term.
52
+ _AT_COMMAND_POS = re.compile(r"(?:^|[|;&]|\$\()\s*(?:sudo\s+)?(?:grep|rg)\s")
53
+
54
+ # 2. A heredoc means most of the command is DATA, not shell — too noisy to parse.
55
+ _HEREDOC = re.compile(r"<<-?['\"]?\w*(?:EOF|PY)")
56
+
57
+ # 3. It should look like a CODE search, not a config/log one.
58
+ _CODEISH = re.compile(
59
+ r"\.(?:py|ts|tsx|js|jsx|go|rs|java|rb|c|h|cpp)\b|--include|(?:^|\s)-r\s|src/"
60
+ )
61
+
62
+
63
+ def _looks_like_code_search(cmd: str) -> bool:
64
+ if not _AT_COMMAND_POS.search(cmd):
65
+ return False
66
+ if _HEREDOC.search(cmd):
67
+ return False
68
+ return bool(_CODEISH.search(cmd))
69
+
70
+
71
+ def extract_term(cmd: str) -> Optional[str]:
72
+ """The search term ADJACENT to grep — not the first quoted string anywhere.
73
+
74
+ A `gh pr close --comment "...'dead'..." && grep foo bar` fired a query for
75
+ 'dead', lifted out of unrelated prose. So the scan starts AT the grep token.
76
+ """
77
+ m = _AT_COMMAND_POS.search(cmd)
78
+ if not m:
79
+ return None
80
+ tail = cmd[m.end():]
81
+ # Skip flags (-n, -R, --include=*.py, …) to reach the pattern itself.
82
+ for tok in _tokenize(tail):
83
+ if tok.startswith("-"):
84
+ continue
85
+ term = tok.strip("'\"")
86
+ # A regex-metachar soup is not a useful symbol query.
87
+ cleaned = re.sub(r"[^\w\s.]", " ", term).strip()
88
+ return cleaned or None
89
+ return None
90
+
91
+
92
+ def _tokenize(s: str) -> list:
93
+ """Shell-ish split that keeps quoted strings together.
94
+
95
+ NOTE: no early break. An earlier version stopped after the FIRST completed
96
+ token, so a command whose first argument is a flag (`grep -rn 'pat' src/`)
97
+ yielded only `-rn`, the caller skipped it as a flag, and extraction returned
98
+ None — the hook silently never fired on the most common grep shape there is.
99
+ Caught by test_skips_flags_to_reach_the_pattern.
100
+ """
101
+ out, cur, quote, started = [], "", None, False
102
+ for ch in s:
103
+ if quote:
104
+ if ch == quote:
105
+ quote = None
106
+ else:
107
+ cur += ch
108
+ elif ch in "'\"":
109
+ quote, started = ch, True
110
+ elif ch.isspace():
111
+ if cur or started:
112
+ out.append(cur)
113
+ cur, started = "", False
114
+ else:
115
+ cur += ch
116
+ started = True
117
+ if cur or started:
118
+ out.append(cur)
119
+ return out
120
+
121
+
122
+ # ── gateway query ─────────────────────────────────────────────────────────
123
+
124
+ def _token_path(self_name: str) -> Path:
125
+ return Path.home() / ".config" / "swarph" / f"{self_name}.peer_token"
126
+
127
+
128
+ def query_gateway(term: str, gateway: str, token: str, limit: int = MAX_ROWS) -> dict:
129
+ """POST /codegraph. Returns the envelope, or {"error": "..."} — NEVER {}.
130
+
131
+ >>> AN UNREACHABLE OR REFUSING BACKEND IS AN ERROR, NOT AN EMPTY RESULT. <<<
132
+ Collapsing them is the defect this hook exists to avoid teaching.
133
+ """
134
+ req = urllib.request.Request(
135
+ f"{gateway.rstrip('/')}/codegraph",
136
+ data=json.dumps({"query": term, "limit": limit}).encode(),
137
+ headers={"Authorization": f"Bearer {token}",
138
+ "Content-Type": "application/json"},
139
+ method="POST",
140
+ )
141
+ try:
142
+ with urllib.request.urlopen(req, timeout=TIMEOUT_S) as r:
143
+ return json.loads(r.read().decode())
144
+ except urllib.error.HTTPError as e:
145
+ detail = ""
146
+ try:
147
+ detail = json.loads(e.read().decode()).get("detail", "")
148
+ except Exception: # noqa: BLE001
149
+ pass
150
+ return {"error": f"HTTP {e.code}: {detail or e.reason}"}
151
+ except Exception as e: # noqa: BLE001 — transport/parse
152
+ return {"error": f"{type(e).__name__}: {e}"}
153
+
154
+
155
+ def render(term: str, env: dict) -> str:
156
+ """Format the envelope for injection. Errors are LOUD and named."""
157
+ if "error" in env:
158
+ return (f"CODEGRAPH UNAVAILABLE for '{term}' — {env['error']}\n"
159
+ f" >>> THIS IS NOT 'no matches'. The structural index could not be "
160
+ f"consulted, so grep's answer is the ONLY answer you have. Treat "
161
+ f"definitions/callers as UNVERIFIED.")
162
+
163
+ rows = env.get("results") or []
164
+ fresh = env.get("freshness") or []
165
+ stale = [f for f in fresh if f.get("stale")]
166
+ age = ""
167
+ if fresh:
168
+ hrs = max((f.get("index_age_hours") or 0) for f in fresh)
169
+ age = f", index {hrs:.1f}h old"
170
+ if stale:
171
+ age += " ⚠ STALE — verify line numbers against the file"
172
+
173
+ if not rows:
174
+ body = (" no structural matches (the index IS present and answered — "
175
+ "a REAL negative)")
176
+ else:
177
+ body = "\n".join(
178
+ f" {r.get('repo')}/{r.get('file_path')}:{r.get('start_line')} "
179
+ f"{r.get('kind')} {r.get('name')} callers={r.get('callers')}"
180
+ for r in rows[:MAX_ROWS]
181
+ )
182
+ return (f"CODEGRAPH (structural{age}) for '{term}' — grep found text; this is "
183
+ f"the symbol graph, incl. CALLER COUNTS grep cannot see:\n{body}\n\n"
184
+ f"Use this for definitions/callers/blast-radius. grep remains correct "
185
+ f"for config, logs and string literals the codegraph does not index.")
186
+
187
+
188
+ def run_codegraph_hook(argv: Optional[list] = None) -> int:
189
+ """PostToolUse(Bash) entry point. ALWAYS exits 0 — it must never fail a turn."""
190
+ argv = list(argv or [])
191
+ self_name = os.environ.get("SWARPH_SELF", "").strip()
192
+ gateway = DEFAULT_GATEWAY
193
+ for i, a in enumerate(argv):
194
+ if a == "--as" and i + 1 < len(argv):
195
+ self_name = argv[i + 1]
196
+ elif a == "--gateway" and i + 1 < len(argv):
197
+ gateway = argv[i + 1]
198
+
199
+ try:
200
+ payload = json.loads(sys.stdin.read() or "{}")
201
+ except Exception: # noqa: BLE001
202
+ return 0
203
+ cmd = ((payload.get("tool_input") or {}).get("command") or "").strip()
204
+ if not cmd or not _looks_like_code_search(cmd):
205
+ return 0
206
+ term = extract_term(cmd)
207
+ if not term:
208
+ return 0
209
+
210
+ if not self_name:
211
+ _emit(f"CODEGRAPH SKIPPED for '{term}' — no cell identity (set SWARPH_SELF "
212
+ f"or pass --as). Not a negative result: the graph was never asked.")
213
+ return 0
214
+ tp = _token_path(self_name)
215
+ try:
216
+ token = tp.read_text().strip()
217
+ except OSError as e:
218
+ _emit(f"CODEGRAPH UNAVAILABLE for '{term}' — peer token unreadable at {tp}: "
219
+ f"{e}. Not a negative result: the graph was never asked.")
220
+ return 0
221
+
222
+ _emit(render(term, query_gateway(term, gateway, token)))
223
+ return 0
224
+
225
+
226
+ def _emit(text: str) -> None:
227
+ """Hand context back to the model via the documented hook JSON shape."""
228
+ print(json.dumps({
229
+ "hookSpecificOutput": {
230
+ "hookEventName": "PostToolUse",
231
+ "additionalContext": text,
232
+ }
233
+ }))
@@ -155,7 +155,43 @@ exit 0
155
155
  """
156
156
 
157
157
 
158
+ # The bundled codegraph-on-grep script (card #194). Deliberately a THIN WRAPPER
159
+ # around `swarph codegraph-hook` rather than fat inline shell: the trigger
160
+ # heuristics are the hard-won part — each guard in that module exists because a
161
+ # LOOSER version misfired in production (a heredoc delimiter extracted as the
162
+ # search term; a quoted string lifted out of unrelated prose) — and heuristics
163
+ # that specific must be unit-testable, which inline sh is not. Same pattern as
164
+ # `install-hook`, which points at `swarph hook-output`.
165
+ #
166
+ # `exec` so the hook process IS the handler, and the verb always exits 0: a
167
+ # PostToolUse hook must never fail a turn.
168
+ _CODEGRAPH_ON_GREP_SH = r"""#!/bin/sh
169
+ # codegraph-on-grep.sh — swarph bundled Claude Code hook (PostToolUse/Bash).
170
+ #
171
+ # When a grep/rg searches CODE, also ask the gateway's structural codegraph and
172
+ # hand the symbol-level answer back as context. Supplements grep, never blocks it.
173
+ # All logic lives in `swarph codegraph-hook` so it can be tested; see that module
174
+ # for why each trigger guard exists.
175
+ exec swarph codegraph-hook "$@"
176
+ """
177
+
158
178
  BUILTIN_HOOKS: dict = {
179
+ "codegraph-on-grep": HookBundle(
180
+ name="codegraph-on-grep",
181
+ description=(
182
+ "When a grep/rg searches code, also queries the gateway's structural "
183
+ "codegraph and injects the symbol-level answer (definitions, file:line, "
184
+ "caller counts) as context. Supplements grep; never blocks it. Reports "
185
+ "an unavailable index LOUDLY rather than as 'no matches'."
186
+ ),
187
+ publisher="swarph-builtin",
188
+ trust="builtin",
189
+ script_name="codegraph-on-grep.sh",
190
+ script_body=_CODEGRAPH_ON_GREP_SH,
191
+ bindings=(
192
+ HookBinding("PostToolUse", "Bash"),
193
+ ),
194
+ ),
159
195
  "cell-resilience": HookBundle(
160
196
  name="cell-resilience",
161
197
  description=(
@@ -1045,16 +1045,93 @@ def _proc_cmdline(pid: int) -> Optional[str]:
1045
1045
  return raw.replace(b"\x00", b" ").decode("utf-8", "replace").strip() or None
1046
1046
 
1047
1047
 
1048
- def _process_alive(pid: int) -> bool:
1048
+ # Liveness is TRI-STATE. "I could not determine" is not "dead" — see _process_liveness.
1049
+ LIVENESS_ALIVE = "alive"
1050
+ LIVENESS_DEAD = "dead"
1051
+ LIVENESS_UNKNOWN = "unknown"
1052
+
1053
+
1054
+ def _windows_liveness(pid: int) -> str:
1055
+ """Ask Windows directly, via OpenProcess — never via os.kill.
1056
+
1057
+ PROCESS_QUERY_LIMITED_INFORMATION (0x1000) is the minimum right that answers
1058
+ "does this pid exist", and it is grantable across sessions where the rights
1059
+ os.kill needs are not. ERROR_ACCESS_DENIED means the process EXISTS and we may
1060
+ not inspect it — that is ALIVE, not dead. ERROR_INVALID_PARAMETER means no such
1061
+ pid. Anything else is UNKNOWN, never dead.
1062
+ """
1063
+ try:
1064
+ import ctypes
1065
+ from ctypes import wintypes
1066
+ except Exception: # noqa: BLE001
1067
+ return LIVENESS_UNKNOWN
1068
+ ERROR_ACCESS_DENIED, ERROR_INVALID_PARAMETER, STILL_ACTIVE = 5, 87, 259
1069
+ try:
1070
+ k32 = ctypes.WinDLL("kernel32", use_last_error=True)
1071
+ handle = k32.OpenProcess(0x1000, False, pid)
1072
+ if not handle:
1073
+ err = ctypes.get_last_error()
1074
+ if err == ERROR_ACCESS_DENIED:
1075
+ return LIVENESS_ALIVE # exists; we just cannot look
1076
+ if err == ERROR_INVALID_PARAMETER:
1077
+ return LIVENESS_DEAD # no such pid
1078
+ return LIVENESS_UNKNOWN
1079
+ try:
1080
+ code = wintypes.DWORD()
1081
+ if not k32.GetExitCodeProcess(handle, ctypes.byref(code)):
1082
+ return LIVENESS_UNKNOWN
1083
+ return LIVENESS_ALIVE if code.value == STILL_ACTIVE else LIVENESS_DEAD
1084
+ finally:
1085
+ k32.CloseHandle(handle)
1086
+ except Exception: # noqa: BLE001
1087
+ return LIVENESS_UNKNOWN
1088
+
1089
+
1090
+ def _process_liveness(pid: int) -> str:
1091
+ """'alive' | 'dead' | 'unknown'.
1092
+
1093
+ >>> CARD #195 — `os.kill(pid, 0)` IS NOT A LIVENESS PROBE ON WINDOWS, AND ITS
1094
+ FAILURE WAS BEING SWALLOWED INTO "DEAD". <<< Measured by workstation-lc against
1095
+ a monitor that was demonstrably draining:
1096
+
1097
+ pid 6120 alive per Windows ................. True
1098
+ os.kill(6120, 0) from another process ...... OSError [WinError 87]
1099
+ -> `except OSError: return False`
1100
+ pid 6120 after the probe ................... STILL ALIVE
1101
+
1102
+ The error appears when probing a process the caller did NOT spawn — the worker
1103
+ is a grandchild of a detached cmd.exe, a different session/handle-rights context.
1104
+ His first throwaway test passed because he probed his own CHILD: a child-process
1105
+ control cannot surface this, which is why it read safe.
1106
+
1107
+ ONE LINE PRODUCED BOTH SYMPTOMS: `monitor status` reported "not running" about
1108
+ every healthy monitor, AND the single-instance guard believed nothing was
1109
+ running, so it never blocked a second start. False negative and duplicate
1110
+ spawning from the same swallow.
1111
+
1112
+ >>> THE RULE: "COULD NOT DETERMINE" MUST NEVER BE REPORTED AS "DEAD" BY A GUARD
1113
+ WHOSE JOB IS TO PREVENT A SECOND START. <<< A blocked start is visible and
1114
+ recoverable; a duplicate is silent and produces two writers over one cursor.
1115
+ So callers treat UNKNOWN as occupied, and `status` says so rather than claiming
1116
+ the process is running.
1117
+ """
1118
+ if sys.platform == "win32":
1119
+ return _windows_liveness(pid)
1049
1120
  try:
1050
1121
  os.kill(pid, 0)
1051
1122
  except ProcessLookupError:
1052
- return False
1123
+ return LIVENESS_DEAD
1053
1124
  except PermissionError:
1054
- return True # exists, owned by another user
1125
+ return LIVENESS_ALIVE # exists, owned by another user
1055
1126
  except OSError:
1056
- return False
1057
- return True
1127
+ return LIVENESS_UNKNOWN # NOT dead — we simply could not tell
1128
+ return LIVENESS_ALIVE
1129
+
1130
+
1131
+ def _process_alive(pid: int) -> bool:
1132
+ """Back-compat bool. UNKNOWN counts as alive — see _process_liveness for why
1133
+ treating an undetermined answer as "dead" is the failure mode, not the safe one."""
1134
+ return _process_liveness(pid) in (LIVENESS_ALIVE, LIVENESS_UNKNOWN)
1058
1135
 
1059
1136
 
1060
1137
  def _terminate(pid: int) -> None:
@@ -40,7 +40,13 @@ def _build_parser() -> argparse.ArgumentParser:
40
40
  "--context",
41
41
  action="append",
42
42
  default=[],
43
- help="context reference (repeatable)",
43
+ metavar="KEY=VALUE",
44
+ help="durable context anchor, repeatable. KEY is one of "
45
+ + "|".join(sorted(DURABLE_ANCHOR_KEYS))
46
+ + " (e.g. --context memory=project_x --context repo=swarph-cli). "
47
+ "A raw JSON object is also accepted. REQUIRED: the gateway rejects an "
48
+ "event with no anchors, because an event that fires with no durable "
49
+ "context wakes a cell that cannot reconstruct why.",
44
50
  )
45
51
  create.add_argument(
46
52
  "--min-interval",
@@ -93,14 +99,81 @@ def _ctx(args: argparse.Namespace) -> tuple[str, str, str]:
93
99
  return self_name, token, base
94
100
 
95
101
 
102
+
103
+ # Mirrors mesh-gateway `_DURABLE_ANCHOR_KEYS` (server.py). Kept in sync by
104
+ # test_schedule_create_contract.py, which fails if this drifts from the value the
105
+ # gateway actually enforces — a duplicated constant nobody checks is how the two
106
+ # sides diverged in the first place.
107
+ DURABLE_ANCHOR_KEYS = frozenset({"repo", "memory", "channel", "feature", "file"})
108
+
109
+
110
+ def parse_context_anchor(raw: str) -> dict:
111
+ """Turn one --context value into the ANCHOR DICT the gateway requires.
112
+
113
+ >>> THE BUG THIS FIXES (card #146): the CLI sent `--context` values through as
114
+ RAW STRINGS while the gateway requires each anchor to be a DICT carrying a
115
+ durable key, and requires the list to be NON-EMPTY. So `swarph schedule
116
+ create` COULD NOT SUCCEED — every invocation 400'd, and every real event on
117
+ this mesh had to be armed by hand over the raw API. <<<
118
+
119
+ That is why dated reminders kept living in DM threads instead of the scheduler:
120
+ the verb that makes a date fire was itself broken, so the cheapest path was
121
+ always to tell a peer and hope.
122
+
123
+ Accepts `key=value` for the five durable kinds, or a raw JSON object for
124
+ anything the shorthand cannot express. Raises ValueError with the valid keys
125
+ listed, LOCALLY — a 400 from the gateway names the rule but not the input that
126
+ broke it, and the user is holding the input.
127
+ """
128
+ raw = (raw or "").strip()
129
+ if not raw:
130
+ raise ValueError("empty --context value")
131
+ if raw.startswith("{"):
132
+ try:
133
+ anchor = json.loads(raw)
134
+ except json.JSONDecodeError as exc:
135
+ raise ValueError(f"--context {raw!r} looks like JSON but does not parse: {exc}") from exc
136
+ if not isinstance(anchor, dict):
137
+ raise ValueError(f"--context JSON must be an object, got {type(anchor).__name__}")
138
+ if not (set(anchor) & DURABLE_ANCHOR_KEYS):
139
+ raise ValueError(
140
+ f"--context {raw!r} has no durable key; one of "
141
+ f"{sorted(DURABLE_ANCHOR_KEYS)} is required")
142
+ return anchor
143
+ key, sep, value = raw.partition("=")
144
+ key, value = key.strip(), value.strip()
145
+ if not sep or not value:
146
+ raise ValueError(
147
+ f"--context {raw!r} must be KEY=VALUE (e.g. memory=project_x); "
148
+ f"KEY is one of {sorted(DURABLE_ANCHOR_KEYS)}")
149
+ if key not in DURABLE_ANCHOR_KEYS:
150
+ raise ValueError(
151
+ f"--context key {key!r} is not durable; use one of {sorted(DURABLE_ANCHOR_KEYS)}. "
152
+ f"An anchor must name something that survives compaction — a /tmp path or a "
153
+ f"session id is exactly what the gateway rejects.")
154
+ return {key: value}
155
+
156
+
96
157
  def _run_create(args: argparse.Namespace) -> int:
97
158
  self_name, token, base = _ctx(args)
159
+ if not args.context:
160
+ print("swarph schedule create: --context is REQUIRED (repeatable).\n"
161
+ " The gateway refuses an event with no durable anchor: an event that\n"
162
+ " fires with no context wakes a cell that cannot reconstruct why it woke.\n"
163
+ f" Use KEY=VALUE with KEY in {sorted(DURABLE_ANCHOR_KEYS)},\n"
164
+ " e.g. --context memory=project_graduation_register", file=sys.stderr)
165
+ return 2
166
+ try:
167
+ anchors = [parse_context_anchor(c) for c in args.context]
168
+ except ValueError as exc:
169
+ print(f"swarph schedule create: {exc}", file=sys.stderr)
170
+ return 2
98
171
  body = {
99
172
  "name": args.name,
100
173
  "trigger_type": args.trigger,
101
174
  "target_cell": args.target,
102
175
  "task": args.task,
103
- "context_ref": args.context,
176
+ "context_ref": anchors,
104
177
  "created_by": self_name,
105
178
  }
106
179
  if args.cron is not None:
@@ -2437,8 +2437,15 @@ def _is_operator(auth: AuthContext) -> bool:
2437
2437
  membership/broadcast authority of the FUTURE POST surface is likewise REAL
2438
2438
  only post-C5 (a cell can already create an announce channel + own
2439
2439
  allow_broadcast=1 on it). Mitigation now: pre-seed the reserved names via the
2440
- operator at deploy so the PK-409 protects the canonical names meanwhile."""
2441
- return auth.peer is None
2440
+ operator at deploy so the PK-409 protects the canonical names meanwhile.
2441
+
2442
+ META-EDGE (B1/B2): a verified SSO login (kind='user_identity') and a
2443
+ cell-join key (kind='cell_join') BOTH carry peer=None, so a bare
2444
+ `peer is None` check made every OAuth login a full operator — an
2445
+ OAuth-driven operator factory. The genuine operator is the shared/commander
2446
+ token ALONE, which is the only context with BOTH peer=None AND kind=None.
2447
+ A Meta-Edge context is owner-scoped, never operator."""
2448
+ return auth.peer is None and auth.kind is None
2442
2449
 
2443
2450
 
2444
2451
  class ChannelCreateRequest(BaseModel):
@@ -84,6 +84,7 @@ _VERB_HANDLERS: dict[str, str] = {
84
84
  "spawn": "swarph_cli.commands.spawn.run_spawn",
85
85
  "install-hook": "swarph_cli.commands.install_hook.run_install_hook",
86
86
  "hook-output": "swarph_cli.commands.hook_output.run_hook_output",
87
+ "codegraph-hook": "swarph_cli.commands.codegraph_hook.run_codegraph_hook",
87
88
  "watchdog": "swarph_cli.commands.watchdog.run_watchdog",
88
89
  "hooks": "swarph_cli.commands.hooks.run_hooks",
89
90
  "protocol-handler": "swarph_cli.commands.protocol_handler.run_protocol_handler",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swarph-cli
3
- Version: 0.40.2
3
+ Version: 0.41.0
4
4
  Summary: The `swarph` binary — a multi-LLM CLI and mesh-gateway client + bundled server (`swarph gateway`): one-shot prompts across providers, interactive `chat`, multi-provider `spawn` (claude/codex/antigravity via a ProviderMembrane with subprocess billing-scrub), `mesh` send/inbox/register, `brain-ask` over the swarph-brain (gbrain) semantic memory, git-backed `assisted_memory`, session `import`, and a stranded-session `watchdog`.
5
5
  Author: Pierre Samson, Claude Opus
6
6
  License: MIT
@@ -46,6 +46,7 @@ src/swarph_cli/commands/cell_selfcheck.py
46
46
  src/swarph_cli/commands/channel.py
47
47
  src/swarph_cli/commands/chat.py
48
48
  src/swarph_cli/commands/codegraph.py
49
+ src/swarph_cli/commands/codegraph_hook.py
49
50
  src/swarph_cli/commands/compress.py
50
51
  src/swarph_cli/commands/daemon.py
51
52
  src/swarph_cli/commands/event.py
@@ -130,7 +131,9 @@ tests/test_channel_post.py
130
131
  tests/test_chat_command.py
131
132
  tests/test_claude_parser.py
132
133
  tests/test_claude_tmux_template.py
134
+ tests/test_codegraph_caller_scope.py
133
135
  tests/test_codegraph_command.py
136
+ tests/test_codegraph_hook.py
134
137
  tests/test_codegraph_mcp.py
135
138
  tests/test_compress_caller_convention.py
136
139
  tests/test_compress_command.py
@@ -184,11 +187,13 @@ tests/test_multiplexer.py
184
187
  tests/test_okf_links.py
185
188
  tests/test_onboard_command.py
186
189
  tests/test_packaged_artifacts_exist.py
190
+ tests/test_process_liveness.py
187
191
  tests/test_protocol_handler.py
188
192
  tests/test_providers_isolation.py
189
193
  tests/test_ratify_command.py
190
194
  tests/test_repl_caller_convention.py
191
195
  tests/test_schedule_command.py
196
+ tests/test_schedule_create_contract.py
192
197
  tests/test_security.py
193
198
  tests/test_service_app.py
194
199
  tests/test_service_command.py
@@ -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.40.2"
20
+ assert swarph_cli.__version__ == "0.41.0"
21
21
 
22
22
 
23
23
  # --- request / response plumbing -------------------------------------------