omnimancer-cli 0.2.27__tar.gz → 0.3.1__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 (199) hide show
  1. {omnimancer_cli-0.2.27/omnimancer_cli.egg-info → omnimancer_cli-0.3.1}/PKG-INFO +1 -1
  2. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/cli/command_dispatch.py +4 -0
  3. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/cli/commands.py +1 -0
  4. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/cli/completion.py +1 -0
  5. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/cli/display.py +10 -0
  6. omnimancer_cli-0.3.1/omnimancer/cli/h2l_command.py +508 -0
  7. omnimancer_cli-0.3.1/omnimancer/cli/h2l_headless.py +379 -0
  8. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/cli/headless.py +295 -30
  9. omnimancer_cli-0.3.1/omnimancer/cli/headless_checkpoint.py +166 -0
  10. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/cli/interface.py +120 -4
  11. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/cli/system_prompts.py +71 -34
  12. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/cli/turn_notify.py +7 -1
  13. omnimancer_cli-0.3.1/omnimancer/cli/usage.py +47 -0
  14. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/config_migration.py +1 -0
  15. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/engine.py +56 -4
  16. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/models.py +11 -0
  17. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/rate_limit_fallback.py +14 -2
  18. omnimancer_cli-0.3.1/omnimancer/h2l/__init__.py +6 -0
  19. omnimancer_cli-0.3.1/omnimancer/h2l/judge.py +245 -0
  20. omnimancer_cli-0.3.1/omnimancer/h2l/loop.py +263 -0
  21. omnimancer_cli-0.3.1/omnimancer/h2l/models.py +477 -0
  22. omnimancer_cli-0.3.1/omnimancer/h2l/orchestrator.py +527 -0
  23. omnimancer_cli-0.3.1/omnimancer/h2l/planner.py +380 -0
  24. omnimancer_cli-0.3.1/omnimancer/h2l/prompts.py +130 -0
  25. omnimancer_cli-0.3.1/omnimancer/h2l/refs.py +135 -0
  26. omnimancer_cli-0.3.1/omnimancer/h2l/render.py +68 -0
  27. omnimancer_cli-0.3.1/omnimancer/h2l/worker.py +273 -0
  28. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/providers/azure.py +7 -0
  29. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/providers/bedrock.py +43 -0
  30. omnimancer_cli-0.3.1/omnimancer/providers/cache_tokens.py +50 -0
  31. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/providers/claude.py +71 -10
  32. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/providers/digitalocean.py +30 -1
  33. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/providers/gemini.py +9 -0
  34. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/providers/mistral.py +7 -0
  35. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/providers/openai.py +34 -2
  36. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/providers/openrouter.py +36 -0
  37. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/providers/vertex.py +7 -0
  38. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/providers/xai.py +7 -0
  39. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/tui/fleet/app.py +2 -10
  40. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1/omnimancer_cli.egg-info}/PKG-INFO +1 -1
  41. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer_cli.egg-info/SOURCES.txt +16 -0
  42. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/pyproject.toml +1 -1
  43. omnimancer_cli-0.3.1/tests/test_claude_prompt_cache.py +158 -0
  44. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_openai_provider_tools.py +60 -0
  45. omnimancer_cli-0.3.1/tests/test_provider_cache_tokens.py +266 -0
  46. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_system_prompts.py +140 -23
  47. omnimancer_cli-0.2.27/omnimancer/cli/usage.py +0 -27
  48. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/LICENSE +0 -0
  49. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/README.md +0 -0
  50. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omn_fleet_hook.py +0 -0
  51. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/__init__.py +0 -0
  52. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/__main__.py +0 -0
  53. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/cli/__init__.py +0 -0
  54. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/cli/agent_loop.py +0 -0
  55. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/cli/approval_formatter.py +0 -0
  56. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/cli/approval_integration.py +0 -0
  57. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/cli/approval_prompt.py +0 -0
  58. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/cli/dynamic_commands.py +0 -0
  59. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/cli/file_mentions.py +0 -0
  60. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/cli/handlers/__init__.py +0 -0
  61. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/cli/prompt_input.py +0 -0
  62. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/cli/pt_completion.py +0 -0
  63. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/cli/session.py +0 -0
  64. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/cli/subagent.py +0 -0
  65. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/cli/tool_handler.py +0 -0
  66. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/__init__.py +0 -0
  67. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/agent/__init__.py +0 -0
  68. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/agent/approval_context.py +0 -0
  69. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/agent/approval_interface.py +0 -0
  70. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/agent/approval_manager.py +0 -0
  71. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/agent/config.py +0 -0
  72. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/agent/file_system_manager.py +0 -0
  73. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/agent/program_executor.py +0 -0
  74. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/agent/read_before_write_errors.py +0 -0
  75. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/agent/status_core.py +0 -0
  76. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/agent/status_manager.py +0 -0
  77. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/agent/tool_definitions.py +0 -0
  78. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/agent/types.py +0 -0
  79. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/agent/web_client.py +0 -0
  80. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/agent/workflow_orchestrator.py +0 -0
  81. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/agent_engine.py +0 -0
  82. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/agent_managers.py +0 -0
  83. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/agent_mode_manager.py +0 -0
  84. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/chat_manager.py +0 -0
  85. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/config_manager.py +0 -0
  86. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/config_validator.py +0 -0
  87. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/conversation_manager.py +0 -0
  88. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/env_loader.py +0 -0
  89. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/fallback_manager.py +0 -0
  90. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/health_monitor.py +0 -0
  91. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/history_manager.py +0 -0
  92. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/hooks.py +0 -0
  93. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/mcp_integration_layer.py +0 -0
  94. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/prompt_enhancer.py +0 -0
  95. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/provider_capabilities.py +0 -0
  96. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/provider_initializer.py +0 -0
  97. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/provider_registry.py +0 -0
  98. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/security/__init__.py +0 -0
  99. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/security/approval_workflow.py +0 -0
  100. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/security/audit_logger.py +0 -0
  101. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/security/permission_controller.py +0 -0
  102. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/security/permission_rules.py +0 -0
  103. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/security/sandbox_manager.py +0 -0
  104. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/security/security_manager.py +0 -0
  105. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/core/signal_handler.py +0 -0
  106. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/events/__init__.py +0 -0
  107. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/events/emitter.py +0 -0
  108. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/events/jsonl_writer.py +0 -0
  109. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/events/schema.py +0 -0
  110. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/mcp/__init__.py +0 -0
  111. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/mcp/client.py +0 -0
  112. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/mcp/manager.py +0 -0
  113. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/providers/__init__.py +0 -0
  114. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/providers/base.py +0 -0
  115. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/providers/claude_code.py +0 -0
  116. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/providers/claude_credentials.py +0 -0
  117. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/providers/cohere.py +0 -0
  118. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/providers/factory.py +0 -0
  119. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/providers/ollama.py +0 -0
  120. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/providers/openai_compatible.py +0 -0
  121. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/providers/perplexity.py +0 -0
  122. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/tui/__init__.py +0 -0
  123. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/tui/fleet/__init__.py +0 -0
  124. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/tui/fleet/cli.py +0 -0
  125. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/tui/fleet/models.py +0 -0
  126. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/tui/fleet/sources.py +0 -0
  127. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/tui/fleet/widgets.py +0 -0
  128. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/ui/__init__.py +0 -0
  129. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/ui/cancellation_handler.py +0 -0
  130. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/ui/progress_indicator.py +0 -0
  131. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/ui/streaming_display.py +0 -0
  132. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/ui/turn_activity.py +0 -0
  133. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/utils/__init__.py +0 -0
  134. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/utils/error_handler.py +0 -0
  135. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/utils/errors.py +0 -0
  136. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer/utils/retry.py +0 -0
  137. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer_cli.egg-info/dependency_links.txt +0 -0
  138. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer_cli.egg-info/entry_points.txt +0 -0
  139. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer_cli.egg-info/requires.txt +0 -0
  140. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/omnimancer_cli.egg-info/top_level.txt +0 -0
  141. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/setup.cfg +0 -0
  142. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_approval_integration_compatibility.py +0 -0
  143. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_approval_preview_format.py +0 -0
  144. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_approval_remember.py +0 -0
  145. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_approval_system.py +0 -0
  146. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_azure_provider.py +0 -0
  147. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_bedrock_provider.py +0 -0
  148. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_claude_code_provider.py +0 -0
  149. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_claude_credentials.py +0 -0
  150. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_claude_provider.py +0 -0
  151. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_cohere_provider.py +0 -0
  152. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_command_approval_security.py +0 -0
  153. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_config_comprehensive.py +0 -0
  154. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_config_migration_passthrough.py +0 -0
  155. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_conversation_history.py +0 -0
  156. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_conversation_manager.py +0 -0
  157. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_core_engine.py +0 -0
  158. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_digitalocean_provider.py +0 -0
  159. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_enhanced_model_info.py +0 -0
  160. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_env_loader.py +0 -0
  161. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_error_handling_robustness.py +0 -0
  162. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_execution_timeout_errors.py +0 -0
  163. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_fallback_manager.py +0 -0
  164. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_fixtures.py +0 -0
  165. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_gemini_provider.py +0 -0
  166. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_hooks.py +0 -0
  167. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_httpx_compatibility.py +0 -0
  168. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_import_validation.py +0 -0
  169. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_interface_smoke_tests.py +0 -0
  170. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_mcp_integration.py +0 -0
  171. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_mcp_integration_layer.py +0 -0
  172. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_mcp_management_commands.py +0 -0
  173. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_mistral_provider.py +0 -0
  174. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_models.py +0 -0
  175. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_ollama_provider.py +0 -0
  176. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_openai_compatible_provider.py +0 -0
  177. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_openai_context_fit.py +0 -0
  178. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_openai_request_timeout.py +0 -0
  179. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_openai_streaming.py +0 -0
  180. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_openrouter_provider.py +0 -0
  181. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_permission_rules.py +0 -0
  182. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_perplexity_provider.py +0 -0
  183. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_program_executor.py +0 -0
  184. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_provider_aliases.py +0 -0
  185. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_provider_base.py +0 -0
  186. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_provider_capability_consistency.py +0 -0
  187. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_provider_discovery.py +0 -0
  188. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_provider_integration.py +0 -0
  189. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_provider_registry.py +0 -0
  190. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_rate_limit_fallback.py +0 -0
  191. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_retry.py +0 -0
  192. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_security_validation.py +0 -0
  193. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_system_integration.py +0 -0
  194. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_tool_calling_integration.py +0 -0
  195. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_tool_handler.py +0 -0
  196. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_vertex_provider.py +0 -0
  197. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_web_request_markers.py +0 -0
  198. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_workflows_comprehensive.py +0 -0
  199. {omnimancer_cli-0.2.27 → omnimancer_cli-0.3.1}/tests/test_xai_provider.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: omnimancer-cli
3
- Version: 0.2.27
3
+ Version: 0.3.1
4
4
  Summary: A unified command-line interface for multiple AI language models
5
5
  Author-email: Kellan Strong <kellan@jite.ai>
6
6
  License: MIT
@@ -138,6 +138,10 @@ class CommandDispatchMixin:
138
138
  await self._handle_subagents_command(command)
139
139
  elif slash_cmd == SlashCommand.FALLBACK:
140
140
  await self._handle_fallback_command(command)
141
+ elif slash_cmd == SlashCommand.H2L:
142
+ from .h2l_command import handle_h2l_command
143
+
144
+ await handle_h2l_command(self, command)
141
145
  elif slash_cmd is not None:
142
146
  self._show_info(f"Command {slash_cmd.value} is not yet implemented")
143
147
 
@@ -65,6 +65,7 @@ class SlashCommand(Enum):
65
65
  PROMPTS = "/prompts"
66
66
  SUBAGENTS = "/subagents"
67
67
  FALLBACK = "/fallback"
68
+ H2L = "/h2l"
68
69
 
69
70
  @classmethod
70
71
  def from_string(cls, command_str: str) -> Optional["SlashCommand"]:
@@ -142,6 +142,7 @@ class CompletionManager:
142
142
  "models": {0: ["refresh"]},
143
143
  "prompts": {0: ["list"]},
144
144
  "subagents": {0: ["list", "run"]},
145
+ "h2l": {0: ["plan", "run", "status", "config"], 1: ["set", "show"]},
145
146
  "validate": {0: ["--fix", "--auto-fix"]},
146
147
  "health": {0: ["--monitor", "--interval"]},
147
148
  "repair": {0: ["--backup", "--force"]},
@@ -252,6 +252,16 @@ Tips:
252
252
  " /subagents list configured subagents\n"
253
253
  " /subagents run <name> <task> run a subagent on a task"
254
254
  ),
255
+ "h2l": (
256
+ "High-to-low: the high model plans and judges, low models "
257
+ "execute stories.\n\n"
258
+ "Usage:\n"
259
+ " /h2l <goal> plan, review the stories, then run\n"
260
+ " /h2l plan <goal> plan only (kept pending)\n"
261
+ " /h2l run run the pending plan\n"
262
+ " /h2l status show the pending plan\n"
263
+ " /h2l config [set <field> <value> ...]"
264
+ ),
255
265
  "validate": (
256
266
  "Validate provider configurations.\n\n"
257
267
  "Usage: /validate [provider] [--fix]"
@@ -0,0 +1,508 @@
1
+ """The ``/h2l`` slash command and its TUI presenter (PRD §7).
2
+
3
+ Kept out of ``command_dispatch.py`` (2.4k lines) on purpose; the dispatcher
4
+ delegates here. The presenter talks to the user through the host CLI's
5
+ console and an injectable ``_h2l_input`` coroutine so tests never touch stdin.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import logging
12
+ import os
13
+ import shlex
14
+ import subprocess
15
+ import tempfile
16
+ import uuid
17
+ from pathlib import Path
18
+ from typing import Any, Dict, List, Optional
19
+
20
+ from rich.panel import Panel
21
+ from rich.table import Table
22
+
23
+ from ..h2l.models import (
24
+ H2LConfig,
25
+ H2LModelRef,
26
+ Plan,
27
+ RunSummary,
28
+ Story,
29
+ StoryOutcome,
30
+ StoryState,
31
+ Verdict,
32
+ WorkerReport,
33
+ )
34
+ from ..h2l.orchestrator import H2LRun, PlanDecision
35
+ from ..h2l.refs import H2LConfigError, validate_tiers
36
+ from ..h2l.render import render_summary_text
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+ USAGE = (
41
+ "Usage:\n"
42
+ " /h2l <goal> plan, review the stories, then run them\n"
43
+ " /h2l plan <goal> plan only; keeps the plan pending\n"
44
+ " /h2l run run the pending plan\n"
45
+ " /h2l status show the pending plan\n"
46
+ " /h2l config show the effective h2l block\n"
47
+ " /h2l config set <field> <value> [<field> <value> ...]"
48
+ )
49
+
50
+ _SETTABLE = {
51
+ "enabled",
52
+ "high",
53
+ "low",
54
+ "max_parallel",
55
+ "max_retries",
56
+ "pass_threshold",
57
+ "worker_tools",
58
+ "allow_worker_read",
59
+ "escalation",
60
+ "plan_approval",
61
+ "worker_max_iterations",
62
+ "planner_max_iterations",
63
+ "high_max_tokens",
64
+ "low_max_tokens",
65
+ }
66
+ _LIST_FIELDS = {"low", "worker_tools"}
67
+ _INT_FIELDS = {
68
+ "max_parallel",
69
+ "max_retries",
70
+ "pass_threshold",
71
+ "worker_max_iterations",
72
+ "planner_max_iterations",
73
+ "high_max_tokens",
74
+ "low_max_tokens",
75
+ }
76
+ # Optional integers: "off"/"none" clears them.
77
+ _OPTIONAL_INT_FIELDS = {"planner_max_iterations", "high_max_tokens", "low_max_tokens"}
78
+ _BOOL_FIELDS = {"enabled", "allow_worker_read", "plan_approval"}
79
+
80
+
81
+ # ---------------------------------------------------------------- presenter
82
+
83
+
84
+ class TUIPresenter:
85
+ def __init__(self, cli: Any) -> None:
86
+ self.cli = cli
87
+ self.console = cli.console
88
+ self.summary: Optional[RunSummary] = None
89
+
90
+ async def _ask(self, prompt: str) -> str:
91
+ asker = getattr(self.cli, "_h2l_input", None)
92
+ if asker is not None:
93
+ answer: str = await asker(prompt)
94
+ return answer
95
+ return await asyncio.to_thread(input, prompt)
96
+
97
+ async def _edit_text(self, text: str) -> str:
98
+ """Open ``$EDITOR`` on the text, or fall back to a multi-line prompt."""
99
+ editor = os.environ.get("EDITOR")
100
+ if editor:
101
+ with tempfile.NamedTemporaryFile(
102
+ "w", suffix=".md", delete=False, encoding="utf-8"
103
+ ) as handle:
104
+ handle.write(text)
105
+ path = handle.name
106
+ try:
107
+ await asyncio.to_thread(
108
+ subprocess.run, [*shlex.split(editor), path], check=False
109
+ )
110
+ return Path(path).read_text(encoding="utf-8")
111
+ finally:
112
+ try:
113
+ os.unlink(path)
114
+ except OSError:
115
+ pass
116
+ self.console.print(
117
+ "Enter the new instructions; finish with a line containing only '.'"
118
+ )
119
+ lines: List[str] = []
120
+ while True:
121
+ line = await self._ask("")
122
+ if line.strip() == ".":
123
+ break
124
+ lines.append(line)
125
+ return "\n".join(lines) or text
126
+
127
+ def _plan_table(self, plan: Plan) -> Table:
128
+ table = Table(title="H2L plan")
129
+ table.add_column("ID", style="cyan")
130
+ table.add_column("Title")
131
+ table.add_column("Files", style="dim")
132
+ table.add_column("Depends", style="magenta")
133
+ table.add_column("Verify", style="dim")
134
+ for story in plan.stories:
135
+ table.add_row(
136
+ story.id,
137
+ story.title,
138
+ "\n".join(story.files),
139
+ ", ".join(story.depends_on) or "-",
140
+ story.verify or "-",
141
+ )
142
+ return table
143
+
144
+ async def on_plan(self, plan: Plan, model_used: str = "") -> None:
145
+ if plan.design:
146
+ title = f"Design (planned by {model_used})" if model_used else "Design"
147
+ self.console.print(Panel(plan.design, title=title))
148
+ self.console.print(self._plan_table(plan))
149
+
150
+ async def approve_plan(self, plan: Plan) -> PlanDecision:
151
+ stories = list(plan.stories)
152
+ while True:
153
+ answer = (
154
+ await self._ask("[a]ccept [d]rop <id> [e]dit <id> [c]ancel > ")
155
+ ).strip()
156
+ parts = answer.split(None, 1)
157
+ verb = parts[0].lower() if parts else ""
158
+ arg = parts[1].strip() if len(parts) > 1 else ""
159
+ if verb in ("a", "accept"):
160
+ if not stories:
161
+ self.cli._show_error("Nothing left to run.")
162
+ return PlanDecision(accepted=False)
163
+ return PlanDecision(
164
+ accepted=True, plan=plan.model_copy(update={"stories": stories})
165
+ )
166
+ if verb in ("c", "cancel"):
167
+ return PlanDecision(accepted=False)
168
+ if verb in ("d", "drop", "e", "edit"):
169
+ target = next((s for s in stories if s.id == arg), None)
170
+ if target is None:
171
+ self.cli._show_error(f"No story '{arg}' in the plan.")
172
+ continue
173
+ if verb in ("d", "drop"):
174
+ stories = [s for s in stories if s.id != arg]
175
+ for s in stories:
176
+ s.depends_on = [d for d in s.depends_on if d != arg]
177
+ self.cli._show_info(f"Dropped {arg}.")
178
+ else:
179
+ new_text = await self._edit_text(target.instructions)
180
+ stories = [
181
+ (
182
+ s.model_copy(update={"instructions": new_text})
183
+ if s.id == arg
184
+ else s
185
+ )
186
+ for s in stories
187
+ ]
188
+ self.cli._show_info(f"Updated {arg}.")
189
+ self.console.print(
190
+ self._plan_table(plan.model_copy(update={"stories": stories}))
191
+ )
192
+ continue
193
+ self.cli._show_error("Answer a, d <id>, e <id>, or c.")
194
+
195
+ async def on_story_start(
196
+ self, story: Story, ref: H2LModelRef, attempt: int, agent_id: str = ""
197
+ ) -> None:
198
+ self.console.print(
199
+ f"[cyan]{story.id}[/cyan] attempt {attempt} on [dim]{ref.label}[/dim]: "
200
+ f"{story.title}"
201
+ )
202
+
203
+ async def on_tool_call(
204
+ self, story: Story, tool_call: Any, result: Any, agent_id: str = ""
205
+ ) -> None:
206
+ status = "[red]error[/red]" if getattr(result, "error", None) else "ok"
207
+ self.console.print(f" [dim]{story.id} › {tool_call.name} {status}[/dim]")
208
+
209
+ async def on_wait(self, label: str, seconds: float) -> None:
210
+ self.console.print(f" [dim]waiting on {label} ({int(seconds)}s)[/dim]")
211
+
212
+ async def on_planner_tool_call(self, tool_call: Any, result: Any) -> None:
213
+ arguments = tool_call.arguments if isinstance(tool_call.arguments, dict) else {}
214
+ if tool_call.name == "submit_plan":
215
+ stories = arguments.get("stories")
216
+ count = len(stories) if isinstance(stories, list) else 0
217
+ target = f"{count} stories"
218
+ else:
219
+ target = (
220
+ arguments.get("file_path")
221
+ or arguments.get("path")
222
+ or arguments.get("pattern")
223
+ or arguments.get("command")
224
+ or ""
225
+ )
226
+ error = getattr(result, "error", None)
227
+ word = "rejected" if tool_call.name == "submit_plan" else "error"
228
+ status = f"[red]{word}: {str(error)[:300]}[/red]" if error else ""
229
+ self.console.print(
230
+ f" [dim]planner › {tool_call.name} {target}[/dim] {status}".rstrip()
231
+ )
232
+
233
+ async def on_report(self, story: Story, report: WorkerReport) -> None:
234
+ verify = (
235
+ f"verify exit {report.verify_exit}"
236
+ if report.verify_exit is not None
237
+ else ""
238
+ )
239
+ files = ", ".join(report.files_changed) or "no files changed"
240
+ self.console.print(f" [dim]{story.id}: {files} {verify}[/dim]")
241
+
242
+ async def on_verdict(self, story: Story, verdict: Verdict) -> None:
243
+ if verdict.passed:
244
+ self.console.print(
245
+ f" [green]{story.id} passed[/green] (judge {verdict.score})"
246
+ )
247
+ else:
248
+ failures = "; ".join(verdict.failures)
249
+ self.console.print(
250
+ f" [yellow]{story.id} failed[/yellow] (judge {verdict.score}, "
251
+ f"{verdict.judge}): {failures}"
252
+ )
253
+
254
+ async def on_escalation(self, story: Story, outcome: StoryOutcome) -> str:
255
+ while True:
256
+ answer = (
257
+ (
258
+ await self._ask(
259
+ f"{story.id} failed {outcome.attempts} attempts. "
260
+ "[h]igh model does it [b]lock [r]etry once more > "
261
+ )
262
+ )
263
+ .strip()
264
+ .lower()
265
+ )
266
+ if answer in ("h", "high"):
267
+ return "high"
268
+ if answer in ("b", "block"):
269
+ return "block"
270
+ if answer in ("r", "retry"):
271
+ return "retry"
272
+ self.cli._show_error("Answer h, b, or r.")
273
+
274
+ async def on_done(self, summary: RunSummary) -> None:
275
+ self.summary = summary
276
+ table = Table(title="H2L summary")
277
+ table.add_column("ID", style="cyan")
278
+ table.add_column("State")
279
+ table.add_column("Attempts", justify="right")
280
+ table.add_column("Model", style="dim")
281
+ table.add_column("Score", justify="right")
282
+ table.add_column("Reason", style="dim")
283
+ styles = {
284
+ StoryState.PASSED: "green",
285
+ StoryState.BLOCKED: "red",
286
+ StoryState.SKIPPED: "yellow",
287
+ StoryState.CANCELLED: "yellow",
288
+ }
289
+ for outcome in summary.stories:
290
+ style = styles.get(outcome.state, "")
291
+ state = (
292
+ f"[{style}]{outcome.state.value}[/{style}]"
293
+ if style
294
+ else outcome.state.value
295
+ )
296
+ table.add_row(
297
+ outcome.id,
298
+ state,
299
+ str(outcome.attempts),
300
+ outcome.model or "-",
301
+ str(outcome.score) if outcome.score is not None else "-",
302
+ outcome.reason or "",
303
+ )
304
+ self.console.print(table)
305
+ high = summary.usage_by_tier.get("high")
306
+ low = summary.usage_by_tier.get("low")
307
+ if high is not None and low is not None:
308
+ self.console.print(
309
+ f"[dim]Tokens high {high.input_tokens} in / {high.output_tokens} out "
310
+ f"(${high.cost_usd:.2f}) low {low.input_tokens} in / "
311
+ f"{low.output_tokens} out (${low.cost_usd:.2f})[/dim]"
312
+ )
313
+
314
+
315
+ # ---------------------------------------------------------------- command
316
+
317
+
318
+ def _session_id(cli: Any) -> str:
319
+ notifier = getattr(cli, "turn_notifier", None)
320
+ session = getattr(notifier, "session_id", None)
321
+ return str(session) if session else uuid.uuid4().hex
322
+
323
+
324
+ def _h2l_config(cli: Any) -> Optional[H2LConfig]:
325
+ config = cli.engine.config_manager.get_config()
326
+ block: Optional[H2LConfig] = getattr(config, "h2l", None)
327
+ if block is None or not block.enabled:
328
+ cli._show_error(
329
+ "H2L is not configured. Add an 'h2l' block to config or run: "
330
+ "/h2l config set high <provider:model> low <provider:model[,...]>"
331
+ )
332
+ return None
333
+ return block
334
+
335
+
336
+ def _build_run(
337
+ cli: Any, config: H2LConfig, presenter: TUIPresenter
338
+ ) -> Optional[H2LRun]:
339
+ try:
340
+ tiers = validate_tiers(config, cli.engine.config_manager, cli.engine)
341
+ except H2LConfigError as exc:
342
+ cli._show_error(str(exc))
343
+ return None
344
+ return H2LRun(
345
+ cli.engine,
346
+ config,
347
+ presenter,
348
+ cwd=Path.cwd(),
349
+ session_id=_session_id(cli),
350
+ tiers=tiers,
351
+ )
352
+
353
+
354
+ def _post_summary(cli: Any, summary: RunSummary) -> None:
355
+ """One assistant message in the parent conversation, never the transcripts."""
356
+ text = "H2L run summary:\n" + render_summary_text(summary)
357
+ try:
358
+ _, model = cli.engine.runtime_identity()
359
+ except Exception:
360
+ model = ""
361
+ chat = getattr(cli.engine, "chat_manager", None)
362
+ if chat is not None:
363
+ chat.add_assistant_message(text, model or "h2l")
364
+
365
+
366
+ async def handle_h2l_command(cli: Any, command: Any) -> None:
367
+ args: List[str] = list(command.args or [])
368
+ if not args:
369
+ cli._show_info(USAGE)
370
+ return
371
+ sub = args[0].lower()
372
+
373
+ if sub == "config":
374
+ await _handle_config(cli, args[1:])
375
+ return
376
+ if sub == "status":
377
+ pending = getattr(cli, "_h2l_pending_plan", None)
378
+ if pending is None:
379
+ cli._show_info("No pending plan. Use '/h2l plan <goal>' to create one.")
380
+ else:
381
+ await TUIPresenter(cli).on_plan(pending)
382
+ cli._show_info("Pending plan ready: '/h2l run' executes it.")
383
+ return
384
+
385
+ config = _h2l_config(cli)
386
+ if config is None:
387
+ return
388
+ presenter = TUIPresenter(cli)
389
+
390
+ if sub == "plan":
391
+ goal = " ".join(args[1:]).strip()
392
+ if not goal:
393
+ cli._show_error("Usage: /h2l plan <goal>")
394
+ return
395
+ run = _build_run(cli, config, presenter)
396
+ if run is None:
397
+ return
398
+ cli._show_info(f"Planning with {config.high.label}…")
399
+ result = await run.plan_goal(goal)
400
+ if result.plan is None:
401
+ cli._show_error(result.error or "H2L: planning failed")
402
+ return
403
+ cli._h2l_pending_plan = result.plan
404
+ await presenter.on_plan(result.plan)
405
+ cli._show_info("Plan pending: '/h2l run' executes it, '/h2l status' shows it.")
406
+ return
407
+
408
+ if sub == "run":
409
+ pending = getattr(cli, "_h2l_pending_plan", None)
410
+ if pending is None:
411
+ cli._show_error("No pending plan. Use '/h2l plan <goal>' first.")
412
+ return
413
+ run = _build_run(cli, config, presenter)
414
+ if run is None:
415
+ return
416
+ summary = await run.run_plan(pending)
417
+ cli._h2l_pending_plan = None
418
+ _post_summary(cli, summary)
419
+ return
420
+
421
+ goal = " ".join(args).strip()
422
+ run = _build_run(cli, config, presenter)
423
+ if run is None:
424
+ return
425
+ cli._show_info(f"Planning with {config.high.label}…")
426
+ summary = await run.run(goal)
427
+ if summary.stop_cause == "plan_failed":
428
+ cli._show_error(summary.error or "H2L: planning failed")
429
+ return
430
+ if summary.stop_cause == "cancelled":
431
+ cli._show_info("H2L run cancelled.")
432
+ return
433
+ _post_summary(cli, summary)
434
+
435
+
436
+ async def _handle_config(cli: Any, args: List[str]) -> None:
437
+ config = cli.engine.config_manager.get_config()
438
+ block = getattr(config, "h2l", None)
439
+ if not args or args[0].lower() == "show":
440
+ if block is None:
441
+ cli._show_info(
442
+ "No h2l block configured. Set both tiers: "
443
+ "/h2l config set high <ref> low <ref[,ref]>"
444
+ )
445
+ return
446
+ table = Table(title="h2l config")
447
+ table.add_column("Field", style="cyan")
448
+ table.add_column("Value")
449
+ for key, current in block.model_dump().items():
450
+ shown: str
451
+ if key == "high":
452
+ shown = block.high.label
453
+ elif key == "low":
454
+ shown = ", ".join(r.label for r in block.low)
455
+ elif isinstance(current, list):
456
+ shown = ", ".join(str(v) for v in current)
457
+ else:
458
+ shown = str(current)
459
+ table.add_row(key, shown)
460
+ cli.console.print(table)
461
+ return
462
+
463
+ if args[0].lower() != "set" or len(args) < 3 or (len(args) - 1) % 2 != 0:
464
+ cli._show_error("Usage: /h2l config set <field> <value> [<field> <value> ...]")
465
+ return
466
+
467
+ data: Dict[str, Any] = block.model_dump() if block is not None else {}
468
+ pairs = list(zip(args[1::2], args[2::2]))
469
+ for key, raw in pairs:
470
+ key = key.lower()
471
+ if key not in _SETTABLE:
472
+ cli._show_error(
473
+ f"Unknown h2l field '{key}'. Fields: {', '.join(sorted(_SETTABLE))}"
474
+ )
475
+ return
476
+ value: Any = raw
477
+ if key in _LIST_FIELDS:
478
+ value = [item.strip() for item in raw.split(",") if item.strip()]
479
+ elif key in _OPTIONAL_INT_FIELDS and raw.lower() in ("off", "none"):
480
+ value = None # unbounded budget / provider's own output cap
481
+ elif key in _INT_FIELDS:
482
+ try:
483
+ value = int(raw)
484
+ except ValueError:
485
+ cli._show_error(f"{key} must be an integer")
486
+ return
487
+ elif key in _BOOL_FIELDS:
488
+ value = raw.lower() in ("1", "true", "yes", "on")
489
+ data[key] = value
490
+
491
+ if block is None and not (data.get("high") and data.get("low")):
492
+ cli._show_error(
493
+ "No h2l block yet: set both tiers at once, e.g. "
494
+ "/h2l config set high claude:claude-opus-5 low gateway:qwen3-8b"
495
+ )
496
+ return
497
+ try:
498
+ new_block = H2LConfig(**data)
499
+ except ValueError as exc:
500
+ cli._show_error(f"Invalid h2l config: {exc}")
501
+ return
502
+ config.h2l = new_block
503
+ try:
504
+ cli.engine.config_manager.save_config(config)
505
+ except Exception as exc:
506
+ cli._show_error(f"Could not save config: {exc}")
507
+ return
508
+ cli._show_success("h2l config updated: " + ", ".join(f"{k}={v}" for k, v in pairs))