code-muse 0.0.1__py3-none-any.whl

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 (394) hide show
  1. code_muse/__init__.py +26 -0
  2. code_muse/__main__.py +10 -0
  3. code_muse/agents/__init__.py +31 -0
  4. code_muse/agents/_builder.py +214 -0
  5. code_muse/agents/_compaction.py +506 -0
  6. code_muse/agents/_diagnostics.py +171 -0
  7. code_muse/agents/_history.py +382 -0
  8. code_muse/agents/_key_listeners.py +148 -0
  9. code_muse/agents/_non_streaming_render.py +148 -0
  10. code_muse/agents/_runtime.py +596 -0
  11. code_muse/agents/agent_creator_agent.py +603 -0
  12. code_muse/agents/agent_helios.py +47 -0
  13. code_muse/agents/agent_manager.py +740 -0
  14. code_muse/agents/agent_muse.py +78 -0
  15. code_muse/agents/agent_planning.py +44 -0
  16. code_muse/agents/agent_qa_melpomene.py +207 -0
  17. code_muse/agents/base_agent.py +194 -0
  18. code_muse/agents/event_stream_handler.py +361 -0
  19. code_muse/agents/json_agent.py +201 -0
  20. code_muse/agents/prompt_v3.py +521 -0
  21. code_muse/agents/subagent_stream_handler.py +273 -0
  22. code_muse/callbacks.py +941 -0
  23. code_muse/chatgpt_codex_client.py +333 -0
  24. code_muse/claude_cache_client.py +853 -0
  25. code_muse/cli_runner/__init__.py +319 -0
  26. code_muse/cli_runner/args.py +63 -0
  27. code_muse/cli_runner/loop.py +510 -0
  28. code_muse/cli_runner/resume.py +72 -0
  29. code_muse/cli_runner/runner.py +161 -0
  30. code_muse/command_line/__init__.py +1 -0
  31. code_muse/command_line/add_model_menu.py +1331 -0
  32. code_muse/command_line/agent_menu.py +674 -0
  33. code_muse/command_line/attachments.py +397 -0
  34. code_muse/command_line/autosave_menu.py +709 -0
  35. code_muse/command_line/clipboard.py +528 -0
  36. code_muse/command_line/colors_menu.py +530 -0
  37. code_muse/command_line/command_handler.py +262 -0
  38. code_muse/command_line/command_registry.py +150 -0
  39. code_muse/command_line/config_commands.py +711 -0
  40. code_muse/command_line/core_commands.py +740 -0
  41. code_muse/command_line/diff_menu.py +865 -0
  42. code_muse/command_line/file_path_completion.py +73 -0
  43. code_muse/command_line/load_context_completion.py +57 -0
  44. code_muse/command_line/model_picker_completion.py +512 -0
  45. code_muse/command_line/model_settings_menu.py +983 -0
  46. code_muse/command_line/onboarding_slides.py +162 -0
  47. code_muse/command_line/onboarding_wizard.py +337 -0
  48. code_muse/command_line/pagination.py +41 -0
  49. code_muse/command_line/pin_command_completion.py +329 -0
  50. code_muse/command_line/prompt_toolkit_completion.py +886 -0
  51. code_muse/command_line/session_commands.py +304 -0
  52. code_muse/command_line/shell_passthrough.py +145 -0
  53. code_muse/command_line/skills_completion.py +158 -0
  54. code_muse/command_line/types.py +18 -0
  55. code_muse/command_line/uc_menu.py +908 -0
  56. code_muse/command_line/utils.py +105 -0
  57. code_muse/command_line/wiggum_state.py +77 -0
  58. code_muse/config.py +1138 -0
  59. code_muse/config_agent.py +168 -0
  60. code_muse/config_appearance.py +241 -0
  61. code_muse/config_model.py +357 -0
  62. code_muse/config_security.py +73 -0
  63. code_muse/error_logging.py +132 -0
  64. code_muse/evals/__init__.py +35 -0
  65. code_muse/evals/eval_helpers.py +81 -0
  66. code_muse/evals/eval_runner.py +299 -0
  67. code_muse/evals/sample_evals/__init__.py +1 -0
  68. code_muse/evals/sample_evals/eval_frugal_reads.py +59 -0
  69. code_muse/evals/sample_evals/eval_memory_planning.py +31 -0
  70. code_muse/evals/sample_evals/eval_shell_efficiency.py +39 -0
  71. code_muse/evals/sample_evals/eval_tool_masking.py +33 -0
  72. code_muse/fs_scan_cache/__init__.py +31 -0
  73. code_muse/fs_scan_cache/invalidation_hooks.py +89 -0
  74. code_muse/fs_scan_cache/scan_cache_core.cpython-314-darwin.so +0 -0
  75. code_muse/fs_scan_cache/scan_cache_core.pyx +203 -0
  76. code_muse/fs_scan_cache/tool_integration.py +309 -0
  77. code_muse/fs_scan_cache/ttl_policy.py +44 -0
  78. code_muse/gemini_code_assist.py +383 -0
  79. code_muse/gemini_model.py +838 -0
  80. code_muse/hook_engine/README.md +105 -0
  81. code_muse/hook_engine/__init__.py +21 -0
  82. code_muse/hook_engine/aliases.py +153 -0
  83. code_muse/hook_engine/engine.py +221 -0
  84. code_muse/hook_engine/executor.py +347 -0
  85. code_muse/hook_engine/matcher.py +154 -0
  86. code_muse/hook_engine/models.py +245 -0
  87. code_muse/hook_engine/registry.py +114 -0
  88. code_muse/hook_engine/trust.py +268 -0
  89. code_muse/hook_engine/validator.py +144 -0
  90. code_muse/http_utils.py +360 -0
  91. code_muse/keymap.py +128 -0
  92. code_muse/list_filtering.py +26 -0
  93. code_muse/main.py +10 -0
  94. code_muse/messaging/__init__.py +259 -0
  95. code_muse/messaging/bus.py +621 -0
  96. code_muse/messaging/commands.py +166 -0
  97. code_muse/messaging/markdown_patches.py +57 -0
  98. code_muse/messaging/message_queue.py +397 -0
  99. code_muse/messaging/messages.py +591 -0
  100. code_muse/messaging/queue_console.py +269 -0
  101. code_muse/messaging/renderers.py +308 -0
  102. code_muse/messaging/rich_renderer.py +1158 -0
  103. code_muse/messaging/shimmer.py +154 -0
  104. code_muse/messaging/spinner/__init__.py +87 -0
  105. code_muse/messaging/spinner/console_spinner.py +250 -0
  106. code_muse/messaging/spinner/spinner_base.py +82 -0
  107. code_muse/messaging/subagent_console.py +458 -0
  108. code_muse/model_factory.py +1203 -0
  109. code_muse/model_switching.py +59 -0
  110. code_muse/model_utils.py +156 -0
  111. code_muse/models.json +66 -0
  112. code_muse/models_cache/__init__.py +26 -0
  113. code_muse/models_cache/blocking_lru_cache.py +98 -0
  114. code_muse/models_cache/cache_writer.py +86 -0
  115. code_muse/models_cache/sha256_hash.cpython-314-darwin.so +0 -0
  116. code_muse/models_cache/sha256_hash.pyx +34 -0
  117. code_muse/models_cache/startup_integration.py +75 -0
  118. code_muse/models_dev_api.json +1 -0
  119. code_muse/models_dev_parser.py +590 -0
  120. code_muse/motion.py +126 -0
  121. code_muse/plugins/__init__.py +471 -0
  122. code_muse/plugins/agent_skills/__init__.py +32 -0
  123. code_muse/plugins/agent_skills/config.py +176 -0
  124. code_muse/plugins/agent_skills/discovery.py +309 -0
  125. code_muse/plugins/agent_skills/downloader.py +389 -0
  126. code_muse/plugins/agent_skills/installer.py +19 -0
  127. code_muse/plugins/agent_skills/metadata.py +293 -0
  128. code_muse/plugins/agent_skills/prompt_builder.py +66 -0
  129. code_muse/plugins/agent_skills/register_callbacks.py +298 -0
  130. code_muse/plugins/agent_skills/remote_catalog.py +320 -0
  131. code_muse/plugins/agent_skills/skill_catalog.py +254 -0
  132. code_muse/plugins/agent_skills/skills_install_menu.py +690 -0
  133. code_muse/plugins/agent_skills/skills_menu.py +791 -0
  134. code_muse/plugins/autonomous_memory/__init__.py +39 -0
  135. code_muse/plugins/autonomous_memory/bm25_scorer.cpython-314-darwin.so +0 -0
  136. code_muse/plugins/autonomous_memory/bm25_scorer.cpython-314-x86_64-linux-gnu.so +0 -0
  137. code_muse/plugins/autonomous_memory/bm25_scorer.pyx +291 -0
  138. code_muse/plugins/autonomous_memory/consolidation.py +82 -0
  139. code_muse/plugins/autonomous_memory/extraction.py +382 -0
  140. code_muse/plugins/autonomous_memory/lease_lock.py +105 -0
  141. code_muse/plugins/autonomous_memory/memory_injection.py +59 -0
  142. code_muse/plugins/autonomous_memory/register_callbacks.py +268 -0
  143. code_muse/plugins/autonomous_memory/secret_scanner.py +62 -0
  144. code_muse/plugins/autonomous_memory/session_scanner.py +163 -0
  145. code_muse/plugins/aws_bedrock/__init__.py +14 -0
  146. code_muse/plugins/aws_bedrock/config.py +99 -0
  147. code_muse/plugins/aws_bedrock/register_callbacks.py +241 -0
  148. code_muse/plugins/aws_bedrock/utils.py +153 -0
  149. code_muse/plugins/azure_foundry/README.md +238 -0
  150. code_muse/plugins/azure_foundry/__init__.py +15 -0
  151. code_muse/plugins/azure_foundry/config.py +125 -0
  152. code_muse/plugins/azure_foundry/discovery.py +187 -0
  153. code_muse/plugins/azure_foundry/register_callbacks.py +495 -0
  154. code_muse/plugins/azure_foundry/token.py +180 -0
  155. code_muse/plugins/azure_foundry/utils.py +345 -0
  156. code_muse/plugins/build_filter/__init__.py +1 -0
  157. code_muse/plugins/build_filter/register_callbacks.py +201 -0
  158. code_muse/plugins/build_filter/strategies/__init__.py +1 -0
  159. code_muse/plugins/build_filter/strategies/build.py +397 -0
  160. code_muse/plugins/chatgpt_oauth/__init__.py +6 -0
  161. code_muse/plugins/chatgpt_oauth/config.py +52 -0
  162. code_muse/plugins/chatgpt_oauth/oauth_flow.py +338 -0
  163. code_muse/plugins/chatgpt_oauth/register_callbacks.py +172 -0
  164. code_muse/plugins/chatgpt_oauth/test_plugin.py +301 -0
  165. code_muse/plugins/chatgpt_oauth/utils.py +538 -0
  166. code_muse/plugins/checkpointing/__init__.py +29 -0
  167. code_muse/plugins/checkpointing/checkpoint_hook.py +51 -0
  168. code_muse/plugins/checkpointing/conversation_snapshots.py +117 -0
  169. code_muse/plugins/checkpointing/register_callbacks.py +51 -0
  170. code_muse/plugins/checkpointing/restore_command.py +263 -0
  171. code_muse/plugins/checkpointing/rewind_shortcut.py +88 -0
  172. code_muse/plugins/checkpointing/shadow_git.py +90 -0
  173. code_muse/plugins/claude_code_hooks/__init__.py +1 -0
  174. code_muse/plugins/claude_code_hooks/config.py +188 -0
  175. code_muse/plugins/claude_code_hooks/register_callbacks.py +208 -0
  176. code_muse/plugins/claude_code_oauth/README.md +167 -0
  177. code_muse/plugins/claude_code_oauth/SETUP.md +93 -0
  178. code_muse/plugins/claude_code_oauth/__init__.py +25 -0
  179. code_muse/plugins/claude_code_oauth/config.py +52 -0
  180. code_muse/plugins/claude_code_oauth/fast_mode.py +124 -0
  181. code_muse/plugins/claude_code_oauth/prompt_handler.py +63 -0
  182. code_muse/plugins/claude_code_oauth/register_callbacks.py +547 -0
  183. code_muse/plugins/claude_code_oauth/test_fast_mode.py +165 -0
  184. code_muse/plugins/claude_code_oauth/test_plugin.py +283 -0
  185. code_muse/plugins/claude_code_oauth/token_refresh_heartbeat.py +237 -0
  186. code_muse/plugins/claude_code_oauth/utils.py +664 -0
  187. code_muse/plugins/copilot_auth/__init__.py +11 -0
  188. code_muse/plugins/copilot_auth/config.py +91 -0
  189. code_muse/plugins/copilot_auth/reasoning_client.py +409 -0
  190. code_muse/plugins/copilot_auth/register_callbacks.py +461 -0
  191. code_muse/plugins/copilot_auth/utils.py +584 -0
  192. code_muse/plugins/custom_commands/__init__.py +14 -0
  193. code_muse/plugins/custom_commands/args_injection.py +82 -0
  194. code_muse/plugins/custom_commands/command_discovery.py +89 -0
  195. code_muse/plugins/custom_commands/command_toml_schema.py +71 -0
  196. code_muse/plugins/custom_commands/register_callbacks.py +176 -0
  197. code_muse/plugins/customizable_commands/__init__.py +0 -0
  198. code_muse/plugins/customizable_commands/register_callbacks.py +136 -0
  199. code_muse/plugins/destructive_command_guard/__init__.py +14 -0
  200. code_muse/plugins/destructive_command_guard/detector.py +375 -0
  201. code_muse/plugins/destructive_command_guard/register_callbacks.py +148 -0
  202. code_muse/plugins/example_custom_command/README.md +280 -0
  203. code_muse/plugins/example_custom_command/register_callbacks.py +51 -0
  204. code_muse/plugins/file_permission_handler/__init__.py +4 -0
  205. code_muse/plugins/file_permission_handler/register_callbacks.py +441 -0
  206. code_muse/plugins/filter_engine/__init__.py +30 -0
  207. code_muse/plugins/filter_engine/classifier.py +153 -0
  208. code_muse/plugins/filter_engine/content_detector.py +184 -0
  209. code_muse/plugins/filter_engine/dispatcher.py +244 -0
  210. code_muse/plugins/filter_engine/register_callbacks.py +188 -0
  211. code_muse/plugins/filter_engine/registry.py +279 -0
  212. code_muse/plugins/filter_engine/strategies/__init__.py +8 -0
  213. code_muse/plugins/filter_engine/strategies/ast_compressor.cpython-314-darwin.so +0 -0
  214. code_muse/plugins/filter_engine/strategies/ast_compressor.cpython-314-x86_64-linux-gnu.so +0 -0
  215. code_muse/plugins/filter_engine/strategies/ast_compressor.pyx +348 -0
  216. code_muse/plugins/filter_engine/strategies/ast_parser.py +167 -0
  217. code_muse/plugins/filter_engine/strategies/code.cpython-314-darwin.so +0 -0
  218. code_muse/plugins/filter_engine/strategies/code.cpython-314-x86_64-linux-gnu.so +0 -0
  219. code_muse/plugins/filter_engine/strategies/code.pyx +584 -0
  220. code_muse/plugins/filter_engine/strategies/git.cpython-314-darwin.so +0 -0
  221. code_muse/plugins/filter_engine/strategies/git.cpython-314-x86_64-linux-gnu.so +0 -0
  222. code_muse/plugins/filter_engine/strategies/git.pyx +438 -0
  223. code_muse/plugins/filter_engine/strategies/json_compressor.cpython-314-darwin.so +0 -0
  224. code_muse/plugins/filter_engine/strategies/json_compressor.pyx +253 -0
  225. code_muse/plugins/filter_engine/strategies/json_patterns.cpython-314-darwin.so +0 -0
  226. code_muse/plugins/filter_engine/strategies/json_patterns.pyx +178 -0
  227. code_muse/plugins/filter_engine/strategies/lint.cpython-314-darwin.so +0 -0
  228. code_muse/plugins/filter_engine/strategies/lint.cpython-314-x86_64-linux-gnu.so +0 -0
  229. code_muse/plugins/filter_engine/strategies/lint.pyx +626 -0
  230. code_muse/plugins/filter_engine/strategies/test.cpython-314-darwin.so +0 -0
  231. code_muse/plugins/filter_engine/strategies/test.cpython-314-x86_64-linux-gnu.so +0 -0
  232. code_muse/plugins/filter_engine/strategies/test.pyx +431 -0
  233. code_muse/plugins/filter_engine/verbosity.py +63 -0
  234. code_muse/plugins/force_push_guard/__init__.py +5 -0
  235. code_muse/plugins/force_push_guard/detector.py +96 -0
  236. code_muse/plugins/force_push_guard/register_callbacks.py +144 -0
  237. code_muse/plugins/force_push_guard/test_detector.py +143 -0
  238. code_muse/plugins/frontend_emitter/__init__.py +25 -0
  239. code_muse/plugins/frontend_emitter/emitter.py +121 -0
  240. code_muse/plugins/frontend_emitter/register_callbacks.py +259 -0
  241. code_muse/plugins/gac/__init__.py +4 -0
  242. code_muse/plugins/gac/git_ops.py +136 -0
  243. code_muse/plugins/gac/prompt.py +191 -0
  244. code_muse/plugins/gac/register_callbacks.py +82 -0
  245. code_muse/plugins/hook_creator/__init__.py +1 -0
  246. code_muse/plugins/hook_creator/register_callbacks.py +34 -0
  247. code_muse/plugins/hook_manager/__init__.py +1 -0
  248. code_muse/plugins/hook_manager/config.py +289 -0
  249. code_muse/plugins/hook_manager/hooks_menu.py +563 -0
  250. code_muse/plugins/hook_manager/register_callbacks.py +227 -0
  251. code_muse/plugins/hook_monitor/register_callbacks.py +36 -0
  252. code_muse/plugins/mindpack/__init__.py +0 -0
  253. code_muse/plugins/mindpack/factory.py +930 -0
  254. code_muse/plugins/mindpack/judge.py +573 -0
  255. code_muse/plugins/mindpack/memory.py +100 -0
  256. code_muse/plugins/mindpack/mindpack_menu.py +1552 -0
  257. code_muse/plugins/mindpack/orchestration.py +605 -0
  258. code_muse/plugins/mindpack/register_callbacks.py +175 -0
  259. code_muse/plugins/mindpack/schemas.py +358 -0
  260. code_muse/plugins/mindpack/tools.py +387 -0
  261. code_muse/plugins/oauth_muse_html.py +226 -0
  262. code_muse/plugins/ollama_setup/__init__.py +5 -0
  263. code_muse/plugins/ollama_setup/completer.py +36 -0
  264. code_muse/plugins/ollama_setup/register_callbacks.py +410 -0
  265. code_muse/plugins/plan_command/__init__.py +0 -0
  266. code_muse/plugins/plan_command/register_callbacks.py +206 -0
  267. code_muse/plugins/plan_mode/__init__.py +37 -0
  268. code_muse/plugins/plan_mode/mode_cycling.py +40 -0
  269. code_muse/plugins/plan_mode/plan_generation.py +68 -0
  270. code_muse/plugins/plan_mode/plan_hooks.py +74 -0
  271. code_muse/plugins/plan_mode/plan_mode_tools.py +138 -0
  272. code_muse/plugins/plan_mode/register_callbacks.py +121 -0
  273. code_muse/plugins/plugin_trust/register_callbacks.py +140 -0
  274. code_muse/plugins/policy_engine/__init__.py +46 -0
  275. code_muse/plugins/policy_engine/approval_flow_integration.py +59 -0
  276. code_muse/plugins/policy_engine/policy_evaluator.py +75 -0
  277. code_muse/plugins/policy_engine/policy_file_discovery.py +90 -0
  278. code_muse/plugins/policy_engine/policy_toml_schema.py +115 -0
  279. code_muse/plugins/policy_engine/register_callbacks.py +112 -0
  280. code_muse/plugins/pop_command/__init__.py +1 -0
  281. code_muse/plugins/pop_command/register_callbacks.py +189 -0
  282. code_muse/plugins/prompt_newline/__init__.py +13 -0
  283. code_muse/plugins/prompt_newline/config.py +19 -0
  284. code_muse/plugins/prompt_newline/register_callbacks.py +159 -0
  285. code_muse/plugins/safety_status/__init__.py +0 -0
  286. code_muse/plugins/safety_status/register_callbacks.py +113 -0
  287. code_muse/plugins/semantic_compression/__init__.py +6 -0
  288. code_muse/plugins/semantic_compression/compressor.py +295 -0
  289. code_muse/plugins/semantic_compression/config.py +123 -0
  290. code_muse/plugins/semantic_compression/register_callbacks.py +320 -0
  291. code_muse/plugins/shell_minimizer/__init__.py +50 -0
  292. code_muse/plugins/shell_minimizer/builtin_filters.toml +393 -0
  293. code_muse/plugins/shell_minimizer/pipeline.py +556 -0
  294. code_muse/plugins/shell_minimizer/primitives.py +482 -0
  295. code_muse/plugins/shell_minimizer/register_callbacks.py +276 -0
  296. code_muse/plugins/shell_safety/__init__.py +6 -0
  297. code_muse/plugins/shell_safety/agent_shell_safety.py +69 -0
  298. code_muse/plugins/shell_safety/command_cache.py +149 -0
  299. code_muse/plugins/shell_safety/register_callbacks.py +202 -0
  300. code_muse/plugins/synthetic_status/__init__.py +1 -0
  301. code_muse/plugins/synthetic_status/register_callbacks.py +128 -0
  302. code_muse/plugins/synthetic_status/status_api.py +145 -0
  303. code_muse/plugins/token_caching/__init__.py +21 -0
  304. code_muse/plugins/token_caching/cache_hit_tracking.py +128 -0
  305. code_muse/plugins/token_caching/cacheable_prefix_detection.py +28 -0
  306. code_muse/plugins/token_caching/register_callbacks.py +54 -0
  307. code_muse/plugins/token_caching/stats_display.py +35 -0
  308. code_muse/plugins/token_tracking/__init__.py +26 -0
  309. code_muse/plugins/token_tracking/database.py +381 -0
  310. code_muse/plugins/token_tracking/edit_analyzer.py +97 -0
  311. code_muse/plugins/token_tracking/record.py +55 -0
  312. code_muse/plugins/token_tracking/register_callbacks.py +277 -0
  313. code_muse/plugins/token_tracking/reports.py +329 -0
  314. code_muse/plugins/universal_constructor/__init__.py +13 -0
  315. code_muse/plugins/universal_constructor/models.py +136 -0
  316. code_muse/plugins/universal_constructor/register_callbacks.py +47 -0
  317. code_muse/plugins/universal_constructor/registry.py +390 -0
  318. code_muse/plugins/universal_constructor/runner.py +474 -0
  319. code_muse/plugins/universal_constructor/safety.py +440 -0
  320. code_muse/plugins/universal_constructor/sandbox.py +584 -0
  321. code_muse/provider_identity.py +105 -0
  322. code_muse/pydantic_patches.py +410 -0
  323. code_muse/reopenable_async_client.py +233 -0
  324. code_muse/round_robin_model.py +151 -0
  325. code_muse/secret_storage.py +74 -0
  326. code_muse/security/__init__.py +1 -0
  327. code_muse/security/redaction.cpython-314-darwin.so +0 -0
  328. code_muse/security/redaction.cpython-314-x86_64-linux-gnu.so +0 -0
  329. code_muse/security/redaction.pyx +135 -0
  330. code_muse/session_storage.py +565 -0
  331. code_muse/status_display.py +261 -0
  332. code_muse/stream_parser/__init__.py +76 -0
  333. code_muse/stream_parser/assistant_text_parser.py +90 -0
  334. code_muse/stream_parser/citation_parser.py +76 -0
  335. code_muse/stream_parser/inline_hidden_tag_parser.py +236 -0
  336. code_muse/stream_parser/proposed_plan_parser.py +158 -0
  337. code_muse/stream_parser/stream_text_chunk.py +23 -0
  338. code_muse/stream_parser/stream_text_parser.py +27 -0
  339. code_muse/stream_parser/tagged_line_parser.cpython-314-darwin.so +0 -0
  340. code_muse/stream_parser/tagged_line_parser.pyx +251 -0
  341. code_muse/stream_parser/utf8_stream_parser.cpython-314-darwin.so +0 -0
  342. code_muse/stream_parser/utf8_stream_parser.pyx +206 -0
  343. code_muse/summarization_agent.py +308 -0
  344. code_muse/terminal_utils.cpython-314-darwin.so +0 -0
  345. code_muse/terminal_utils.cpython-314-x86_64-linux-gnu.so +0 -0
  346. code_muse/terminal_utils.pyx +483 -0
  347. code_muse/tools/__init__.py +459 -0
  348. code_muse/tools/agent_tools.py +613 -0
  349. code_muse/tools/ask_user_question/__init__.py +26 -0
  350. code_muse/tools/ask_user_question/constants.py +73 -0
  351. code_muse/tools/ask_user_question/demo_tui.py +55 -0
  352. code_muse/tools/ask_user_question/handler.py +232 -0
  353. code_muse/tools/ask_user_question/models.py +302 -0
  354. code_muse/tools/ask_user_question/registration.py +37 -0
  355. code_muse/tools/ask_user_question/renderers.py +336 -0
  356. code_muse/tools/ask_user_question/terminal_ui.py +327 -0
  357. code_muse/tools/ask_user_question/theme.py +156 -0
  358. code_muse/tools/ask_user_question/tui_loop.py +422 -0
  359. code_muse/tools/background_jobs.py +99 -0
  360. code_muse/tools/browser/__init__.py +37 -0
  361. code_muse/tools/browser/browser_control.py +289 -0
  362. code_muse/tools/browser/browser_interactions.py +545 -0
  363. code_muse/tools/browser/browser_locators.py +640 -0
  364. code_muse/tools/browser/browser_manager.py +376 -0
  365. code_muse/tools/browser/browser_navigation.py +251 -0
  366. code_muse/tools/browser/browser_screenshot.py +180 -0
  367. code_muse/tools/browser/browser_scripts.py +462 -0
  368. code_muse/tools/browser/browser_workflows.py +222 -0
  369. code_muse/tools/chrome_cdp/__init__.py +1070 -0
  370. code_muse/tools/chrome_cdp/register_callbacks.py +61 -0
  371. code_muse/tools/command_runner.py +1401 -0
  372. code_muse/tools/common.py +1407 -0
  373. code_muse/tools/display.py +87 -0
  374. code_muse/tools/file_modifications.py +1099 -0
  375. code_muse/tools/file_operations.py +860 -0
  376. code_muse/tools/image_tools.py +185 -0
  377. code_muse/tools/meetin_proxy/__init__.py +243 -0
  378. code_muse/tools/meetin_proxy/capture_addon.py +82 -0
  379. code_muse/tools/meetin_proxy/proxy_manager.py +326 -0
  380. code_muse/tools/meetin_proxy/register_callbacks.py +45 -0
  381. code_muse/tools/path_policy.py +219 -0
  382. code_muse/tools/skills_tools.py +586 -0
  383. code_muse/tools/subagent_context.py +158 -0
  384. code_muse/tools/tools_content.py +50 -0
  385. code_muse/tools/universal_constructor.py +965 -0
  386. code_muse/uvx_detection.py +241 -0
  387. code_muse/version_checker.py +86 -0
  388. code_muse-0.0.1.data/data/code_muse/models.json +66 -0
  389. code_muse-0.0.1.data/data/code_muse/models_dev_api.json +1 -0
  390. code_muse-0.0.1.dist-info/METADATA +845 -0
  391. code_muse-0.0.1.dist-info/RECORD +394 -0
  392. code_muse-0.0.1.dist-info/WHEEL +4 -0
  393. code_muse-0.0.1.dist-info/entry_points.txt +2 -0
  394. code_muse-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,605 @@
1
+ """MindPack orchestration — MindPackOrchestrator lifecycle manager.
2
+
3
+ Orchestrates the full consultation lifecycle:
4
+ 1. Receive a consultation request (AskMindPackInput).
5
+ 2. Select and spawn expert agents via ExpertAgentFactory.
6
+ 3. Collect their reports into the ReportStore.
7
+ 4. Invoke the judge merger to produce a unified AskMindPackOutput.
8
+ 5. Clean up session state.
9
+
10
+ Data models live in ``schemas.py`` to avoid circular imports.
11
+ """
12
+
13
+ import asyncio
14
+ import json
15
+ import logging
16
+ import uuid
17
+ from abc import ABC, abstractmethod
18
+ from pathlib import Path
19
+ from typing import TYPE_CHECKING
20
+
21
+ from code_muse.plugins.mindpack.memory import ReportStore
22
+ from code_muse.plugins.mindpack.schemas import (
23
+ AskMindPackInput,
24
+ AskMindPackOutput,
25
+ ExpertDescriptor,
26
+ MindPackConfig,
27
+ MindPackNestedConfig,
28
+ MindPackRankedOption,
29
+ ProfileDescriptor,
30
+ )
31
+ from code_muse.plugins.mindpack.schemas import MindPackExpertReport as ExpertReport
32
+
33
+ if TYPE_CHECKING:
34
+ from code_muse.plugins.mindpack.factory import ExpertAgentFactory
35
+ from code_muse.plugins.mindpack.tools import MindPackInvocationContext
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # Expert selector (pluggable strategy)
41
+ # ---------------------------------------------------------------------------
42
+
43
+
44
+ class ExpertSelector(ABC):
45
+ """Strategy for choosing which experts to consult given an input."""
46
+
47
+ @abstractmethod
48
+ def select(
49
+ self, request: AskMindPackInput, registry: list[ExpertDescriptor]
50
+ ) -> list[ExpertDescriptor]:
51
+ """Return the subset of experts to consult for this request."""
52
+
53
+
54
+ class DefaultExpertSelector(ExpertSelector):
55
+ """Simple selector: returns up to `max_experts` from the registry.
56
+
57
+ For now this is a naive top-N slice. Smarter selection (semantic
58
+ matching, relevance scoring) will come in a later epic.
59
+ """
60
+
61
+ def select(
62
+ self, request: AskMindPackInput, registry: list[ExpertDescriptor]
63
+ ) -> list[ExpertDescriptor]:
64
+ cap = request.max_experts or len(registry)
65
+ return registry[:cap]
66
+
67
+
68
+ # ---------------------------------------------------------------------------
69
+ # Judge merger (pluggable strategy)
70
+ # ---------------------------------------------------------------------------
71
+
72
+
73
+ class JudgeMerger(ABC):
74
+ """Strategy for merging expert reports into a final advisory output.
75
+
76
+ All implementations must provide the async ``merge`` method. The
77
+ ``session_id`` parameter allows mergers to log and emit events in the
78
+ correct consultation context.
79
+ """
80
+
81
+ @abstractmethod
82
+ async def merge(
83
+ self,
84
+ request: AskMindPackInput,
85
+ reports: list[ExpertReport],
86
+ session_id: str,
87
+ ) -> AskMindPackOutput:
88
+ """Produce a unified advisory from all expert reports."""
89
+
90
+
91
+ class PlaceholderJudgeMerger(JudgeMerger):
92
+ """Minimal placeholder that echoes back a summary without real merging.
93
+
94
+ Kept as an explicit opt-out for cases where the LLM judge should
95
+ not be used (e.g. testing, offline mode). The ``LLMJudgeMerger``
96
+ is the default production implementation.
97
+ """
98
+
99
+ async def merge(
100
+ self,
101
+ request: AskMindPackInput,
102
+ reports: list[ExpertReport],
103
+ session_id: str,
104
+ ) -> AskMindPackOutput:
105
+ expert_names = [r.expert_id for r in reports]
106
+ all_risks = [r for report in reports for r in report.risks]
107
+ all_files = [f for report in reports for f in report.files_to_inspect]
108
+ all_recs = [r for report in reports for r in report.proposed_plan]
109
+
110
+ return AskMindPackOutput(
111
+ summary=(
112
+ f"Placeholder merger: consulted {len(reports)} expert(s) "
113
+ f"for '{request.desired_output}' on: "
114
+ f"{request.problem_statement[:200]}"
115
+ ),
116
+ recommended_plan="\n".join(all_recs) or "No recommendations produced.",
117
+ ranked_options=[
118
+ MindPackRankedOption(
119
+ rank=1,
120
+ title="Placeholder option",
121
+ source_experts=expert_names,
122
+ summary="Placeholder merged option — LLM judge not active.",
123
+ )
124
+ ],
125
+ risks=all_risks or ["Placeholder: no risks identified."],
126
+ tests_to_run=[],
127
+ files_to_inspect_or_change=list(dict.fromkeys(all_files)),
128
+ expert_consensus=(f"Placeholder: {len(reports)} expert(s) consulted."),
129
+ disagreements=[],
130
+ confidence=sum(r.confidence for r in reports) / max(len(reports), 1),
131
+ )
132
+
133
+
134
+ # ---------------------------------------------------------------------------
135
+ # MindPackOrchestrator
136
+ # ---------------------------------------------------------------------------
137
+
138
+
139
+ class MindPackOrchestrator:
140
+ """Manages the lifecycle of a MindPack consultation.
141
+
142
+ Typical usage::
143
+
144
+ orchestrator = MindPackOrchestrator()
145
+ result = await orchestrator.consult(request)
146
+ """
147
+
148
+ def __init__(
149
+ self,
150
+ report_store: ReportStore | None = None,
151
+ expert_selector: ExpertSelector | None = None,
152
+ judge_merger: JudgeMerger | None = None,
153
+ expert_factory: ExpertAgentFactory | None = None,
154
+ config: MindPackConfig | None = None,
155
+ ) -> None:
156
+ self._store = report_store or ReportStore()
157
+ self._selector = expert_selector or DefaultExpertSelector()
158
+ if judge_merger is not None:
159
+ self._merger = judge_merger
160
+ else:
161
+ from code_muse.plugins.mindpack.judge import LLMJudgeMerger
162
+
163
+ self._merger = LLMJudgeMerger()
164
+ if expert_factory is not None:
165
+ self._factory = expert_factory
166
+ else:
167
+ from code_muse.plugins.mindpack.factory import ExpertAgentFactory
168
+
169
+ self._factory = ExpertAgentFactory()
170
+ self._expert_registry: list[ExpertDescriptor] = []
171
+ self._profile_registry: list[ProfileDescriptor] = []
172
+ self.config: MindPackConfig = config if config is not None else MindPackConfig()
173
+
174
+ # -- registry -----------------------------------------------------------
175
+
176
+ @staticmethod
177
+ def _get_experts_config_path() -> Path:
178
+ """Return the path to the experts config file."""
179
+ config_dir = Path.home() / ".muse" / "mindpack"
180
+ config_dir.mkdir(parents=True, exist_ok=True)
181
+ return config_dir / "experts.json"
182
+
183
+ def save_experts(self) -> None:
184
+ """Persist current expert registry to disk."""
185
+ config_path = self._get_experts_config_path()
186
+ data = [e.model_dump() for e in self._expert_registry]
187
+ with open(config_path, "w", encoding="utf-8") as f:
188
+ json.dump(data, f, indent=2, ensure_ascii=False)
189
+ logger.info("Orchestrator: saved %d experts to %s", len(data), config_path)
190
+
191
+ def load_experts(self) -> None:
192
+ """Load custom experts from disk, merging with defaults.
193
+
194
+ Preserves experts that were registered before this call
195
+ (i.e., the default experts from tools.py). Custom experts
196
+ from the config file are added, and any experts with the
197
+ same name are replaced.
198
+ """
199
+ config_path = self._get_experts_config_path()
200
+ if not config_path.exists():
201
+ logger.debug("Orchestrator: no custom experts config found")
202
+ return
203
+
204
+ try:
205
+ with open(config_path, encoding="utf-8") as f:
206
+ data = json.load(f)
207
+
208
+ # Remove any existing experts that will be replaced
209
+ custom_experts = [ExpertDescriptor(**d) for d in data]
210
+ names_to_replace = {e.name for e in custom_experts}
211
+ self._expert_registry = [
212
+ e for e in self._expert_registry if e.name not in names_to_replace
213
+ ]
214
+
215
+ # Add custom experts
216
+ self._expert_registry.extend(custom_experts)
217
+ logger.info(
218
+ "Orchestrator: loaded %d custom expert(s) from %s",
219
+ len(custom_experts),
220
+ config_path,
221
+ )
222
+ except Exception as exc:
223
+ logger.error("Orchestrator: failed to load experts config: %s", exc)
224
+
225
+ def register_expert(self, expert: ExpertDescriptor) -> None:
226
+ """Add an expert to the available pool."""
227
+ self._expert_registry.append(expert)
228
+ logger.debug("Orchestrator: registered expert '%s'", expert.name)
229
+
230
+ def register_experts(self, experts: list[ExpertDescriptor]) -> None:
231
+ """Bulk-register a list of experts."""
232
+ for e in experts:
233
+ self.register_expert(e)
234
+
235
+ def remove_expert(self, name: str) -> bool:
236
+ """Remove an expert from the registry by name.
237
+
238
+ Returns:
239
+ True if an expert was removed, False if not found.
240
+ """
241
+ original_len = len(self._expert_registry)
242
+ self._expert_registry = [e for e in self._expert_registry if e.name != name]
243
+ removed = len(self._expert_registry) < original_len
244
+ if removed:
245
+ logger.debug("Orchestrator: removed expert '%s'", name)
246
+ return removed
247
+
248
+ @property
249
+ def expert_registry(self) -> list[ExpertDescriptor]:
250
+ """Read-only view of the current expert pool.
251
+
252
+ When an active profile is set, returns only the experts
253
+ belonging to that profile. Otherwise returns all experts.
254
+ """
255
+ active = self.get_active_profile_name()
256
+ if active:
257
+ return self.get_experts_for_profile(active)
258
+ return list(self._expert_registry)
259
+
260
+ # -- active profile ----------------------------------------------------
261
+
262
+ @staticmethod
263
+ def _get_active_profile_path() -> Path:
264
+ """Return the path to the active profile marker file."""
265
+ config_dir = Path.home() / ".muse" / "mindpack"
266
+ config_dir.mkdir(parents=True, exist_ok=True)
267
+ return config_dir / "active_profile.txt"
268
+
269
+ def get_active_profile_name(self) -> str | None:
270
+ """Return the currently active profile name, or None."""
271
+ path = self._get_active_profile_path()
272
+ if not path.exists():
273
+ return None
274
+ try:
275
+ return path.read_text().strip() or None
276
+ except Exception:
277
+ return None
278
+
279
+ def set_active_profile(self, profile_name: str) -> None:
280
+ """Set the active profile name, persisting to disk."""
281
+ path = self._get_active_profile_path()
282
+ path.write_text(profile_name)
283
+ logger.info("Orchestrator: active profile set to '%s'", profile_name)
284
+
285
+ def clear_active_profile(self) -> None:
286
+ """Clear the active profile."""
287
+ path = self._get_active_profile_path()
288
+ if path.exists():
289
+ path.unlink()
290
+ logger.info("Orchestrator: active profile cleared")
291
+
292
+ # -- profile registry ---------------------------------------------------
293
+
294
+ @staticmethod
295
+ def _get_profiles_config_path() -> Path:
296
+ """Return the path to the profiles config file."""
297
+ config_dir = Path.home() / ".muse" / "mindpack"
298
+ config_dir.mkdir(parents=True, exist_ok=True)
299
+ return config_dir / "profiles.json"
300
+
301
+ def save_profiles(self) -> None:
302
+ """Persist current profile registry to disk."""
303
+ config_path = self._get_profiles_config_path()
304
+ data = [p.model_dump() for p in self._profile_registry]
305
+ with open(config_path, "w", encoding="utf-8") as f:
306
+ json.dump(data, f, indent=2, ensure_ascii=False)
307
+ logger.info("Orchestrator: saved %d profiles to %s", len(data), config_path)
308
+
309
+ def load_profiles(self) -> None:
310
+ """Load profiles from disk, seeding defaults on first run.
311
+
312
+ On first run (no profiles.json), seeds default profiles.
313
+ Auto-migrates: any experts not referenced by any profile
314
+ get added to the 'Default' profile.
315
+ """
316
+ config_path = self._get_profiles_config_path()
317
+ if not config_path.exists():
318
+ logger.info("Orchestrator: no profiles config found — seeding defaults")
319
+ from code_muse.plugins.mindpack.schemas import _DEFAULT_PROFILES
320
+
321
+ self._profile_registry = list(_DEFAULT_PROFILES)
322
+ self._migrate_orphans_to_default()
323
+ self.save_profiles()
324
+ return
325
+
326
+ try:
327
+ with open(config_path, encoding="utf-8") as f:
328
+ data = json.load(f)
329
+ self._profile_registry = [ProfileDescriptor(**d) for d in data]
330
+ self._migrate_orphans_to_default()
331
+ logger.info(
332
+ "Orchestrator: loaded %d profile(s) from %s",
333
+ len(self._profile_registry),
334
+ config_path,
335
+ )
336
+ except Exception as exc:
337
+ logger.error("Orchestrator: failed to load profiles config: %s", exc)
338
+ from code_muse.plugins.mindpack.schemas import _DEFAULT_PROFILES
339
+
340
+ self._profile_registry = list(_DEFAULT_PROFILES)
341
+
342
+ def _migrate_orphans_to_default(self) -> None:
343
+ """Add experts not referenced by any profile to the 'Default' profile."""
344
+ referenced = set()
345
+ for profile in self._profile_registry:
346
+ referenced.update(profile.expert_names)
347
+
348
+ orphan_names = [
349
+ e.name for e in self._expert_registry if e.name not in referenced
350
+ ]
351
+
352
+ if not orphan_names:
353
+ return
354
+
355
+ # Find or create the Default profile
356
+ default = next((p for p in self._profile_registry if p.name == "Default"), None)
357
+ if default is None:
358
+ default = ProfileDescriptor(
359
+ name="Default",
360
+ description="Catch-all for experts not in any other profile",
361
+ expert_names=[],
362
+ )
363
+ self._profile_registry.append(default)
364
+
365
+ for name in orphan_names:
366
+ if name not in default.expert_names:
367
+ default.expert_names.append(name)
368
+ logger.debug(
369
+ "Orchestrator: migrated orphan expert '%s' into Default profile",
370
+ name,
371
+ )
372
+
373
+ def register_profile(self, profile: ProfileDescriptor) -> None:
374
+ """Add a profile to the registry (replaces existing with same name)."""
375
+ self._profile_registry = [
376
+ p for p in self._profile_registry if p.name != profile.name
377
+ ]
378
+ self._profile_registry.append(profile)
379
+ logger.debug("Orchestrator: registered profile '%s'", profile.name)
380
+
381
+ def remove_profile(self, name: str) -> bool:
382
+ """Remove a profile by name.
383
+
384
+ Returns:
385
+ True if a profile was removed, False if not found.
386
+ """
387
+ original_len = len(self._profile_registry)
388
+ self._profile_registry = [p for p in self._profile_registry if p.name != name]
389
+ removed = len(self._profile_registry) < original_len
390
+ if removed:
391
+ logger.debug("Orchestrator: removed profile '%s'", name)
392
+ return removed
393
+
394
+ def get_experts_for_profile(self, profile_name: str) -> list[ExpertDescriptor]:
395
+ """Resolve a profile name to its ExpertDescriptors.
396
+
397
+ Returns an empty list if the profile is not found.
398
+ """
399
+ profile = next(
400
+ (p for p in self._profile_registry if p.name == profile_name), None
401
+ )
402
+ if profile is None:
403
+ return []
404
+
405
+ expert_map = {e.name: e for e in self._expert_registry}
406
+ return [expert_map[name] for name in profile.expert_names if name in expert_map]
407
+
408
+ @property
409
+ def profile_registry(self) -> list[ProfileDescriptor]:
410
+ """Read-only view of the current profile pool."""
411
+ return list(self._profile_registry)
412
+
413
+ # -- consultation lifecycle ---------------------------------------------
414
+
415
+ async def consult(
416
+ self,
417
+ request: AskMindPackInput,
418
+ invocation_context: MindPackInvocationContext | None = None,
419
+ nested_config: MindPackNestedConfig | None = None,
420
+ ) -> AskMindPackOutput:
421
+ """Run a full consultation cycle.
422
+
423
+ 1. Generate a session ID.
424
+ 2. Select experts from the registry.
425
+ 3. Spawn experts and collect reports.
426
+ 4. Run the judge merger.
427
+ 5. Clean up session data.
428
+
429
+ Args:
430
+ request: The consultation input.
431
+ invocation_context: Nested-workflow tracking (depth, call counts).
432
+ nested_config: INI-driven nested limits (timeout, max_depth).
433
+
434
+ Returns the merged advisory output.
435
+ """
436
+ session_id = uuid.uuid4().hex[:12]
437
+ if invocation_context is not None:
438
+ logger.info(
439
+ "Orchestrator: nested consultation depth=%d/%d session='%s'",
440
+ invocation_context.nested_depth,
441
+ invocation_context.max_depth,
442
+ session_id,
443
+ )
444
+ logger.info(
445
+ "Orchestrator: starting consultation session='%s' "
446
+ "desired_output='%s' problem='%s'",
447
+ session_id,
448
+ request.desired_output,
449
+ request.problem_statement[:120],
450
+ )
451
+
452
+ # Select experts
453
+ selected = self._selector.select(request, self._expert_registry)
454
+ logger.info(
455
+ "Orchestrator: selected %d expert(s) for session '%s': %s",
456
+ len(selected),
457
+ session_id,
458
+ [e.name for e in selected],
459
+ )
460
+
461
+ # Spawn experts and collect reports (with optional timeout cap)
462
+ timeout = None
463
+ if nested_config is not None:
464
+ timeout = nested_config.timeout_sec
465
+
466
+ try:
467
+ if timeout:
468
+ reports = await asyncio.wait_for(
469
+ self._spawn_and_collect(session_id, request, selected),
470
+ timeout=timeout,
471
+ )
472
+ else:
473
+ reports = await self._spawn_and_collect(session_id, request, selected)
474
+ except TimeoutError:
475
+ logger.error(
476
+ "Orchestrator: consultation session='%s' exceeded timeout (%ss)",
477
+ session_id,
478
+ timeout,
479
+ )
480
+ # Return a graceful timeout advisory instead of crashing
481
+ return AskMindPackOutput(
482
+ summary=f"MindPack consultation timed out after {timeout}s.",
483
+ recommended_plan=(
484
+ "The expert panel did not finish in time. "
485
+ "Consider simplifying the problem statement or increasing "
486
+ "packmind_nested_timeout_sec in your config."
487
+ ),
488
+ ranked_options=[],
489
+ risks=[f"Timeout: expert pool exceeded {timeout}s limit"],
490
+ tests_to_run=[],
491
+ files_to_inspect_or_change=[],
492
+ expert_consensus="No consensus — consultation aborted by timeout.",
493
+ disagreements=[],
494
+ confidence=0.0,
495
+ )
496
+
497
+ # Merge via judge (async — LLM-backed or placeholder)
498
+ output = await self._merger.merge(request, reports, session_id)
499
+
500
+ # Clean up session memory
501
+ self._store.clear_run(session_id)
502
+
503
+ logger.info(
504
+ "Orchestrator: consultation complete session='%s' "
505
+ "experts=%d confidence=%.2f",
506
+ session_id,
507
+ len(reports),
508
+ output.confidence,
509
+ )
510
+ return output
511
+
512
+ # -- expert spawning (skeleton) -----------------------------------------
513
+
514
+ async def _spawn_and_collect(
515
+ self,
516
+ session_id: str,
517
+ request: AskMindPackInput,
518
+ experts: list[ExpertDescriptor],
519
+ ) -> list[ExpertReport]:
520
+ """Spawn each expert agent and collect their reports in parallel.
521
+
522
+ Uses the Factory to prepare and run experts concurrently via asyncio.gather.
523
+ """
524
+ if hasattr(self._factory, "invoke_expert"):
525
+ # If the factory doesn't support batching directly, use TaskGroup
526
+ async with asyncio.TaskGroup() as tg:
527
+ tg_tasks = [
528
+ tg.create_task(self._invoke_expert(session_id, request, expert))
529
+ for expert in experts
530
+ ]
531
+ results = [t.result() for t in tg_tasks]
532
+ reports = [r for r in results if r is not None]
533
+ for report in reports:
534
+ self._store.add_report(report)
535
+ return reports
536
+
537
+ # Fallback to serial for safe implementation if batch API changes
538
+ return await self._spawn_and_collect_serial(session_id, request, experts)
539
+
540
+ async def _spawn_and_collect_serial(
541
+ self,
542
+ session_id: str,
543
+ request: AskMindPackInput,
544
+ experts: list[ExpertDescriptor],
545
+ ) -> list[ExpertReport]:
546
+ """Serial fallback for expert invocation."""
547
+ reports: list[ExpertReport] = []
548
+ for expert in experts:
549
+ report = await self._invoke_expert(session_id, request, expert)
550
+ if report is not None:
551
+ self._store.add_report(report)
552
+ reports.append(report)
553
+ return reports
554
+
555
+ async def _invoke_expert(
556
+ self,
557
+ session_id: str,
558
+ request: AskMindPackInput,
559
+ expert: ExpertDescriptor,
560
+ ) -> ExpertReport | None:
561
+ """Invoke a single expert via the ExpertAgentFactory.
562
+
563
+ The factory creates a read-only sub-agent with the expert's
564
+ system prompt fragment, runs it against the consultation prompt,
565
+ and returns a structured ``ExpertReport``.
566
+
567
+ Falls back to a minimal error report if the factory fails entirely.
568
+ """
569
+ logger.info(
570
+ "Orchestrator: invoking expert '%s' for session '%s'",
571
+ expert.name,
572
+ session_id,
573
+ )
574
+
575
+ try:
576
+ report = await self._factory.invoke_expert(expert, request, session_id)
577
+ except Exception as exc:
578
+ logger.error(
579
+ "Orchestrator: expert '%s' raised unexpected error: %s",
580
+ expert.name,
581
+ exc,
582
+ exc_info=True,
583
+ )
584
+ report = ExpertReport(
585
+ expert_id=expert.name,
586
+ run_id=session_id,
587
+ lens="error",
588
+ prompt_variant="fallback",
589
+ summary=f"[Error] Unexpected failure: {exc}",
590
+ findings=[],
591
+ proposed_plan=[],
592
+ risks=[f"Expert invocation raised: {exc}"],
593
+ files_to_inspect=[],
594
+ confidence=0.0,
595
+ status="failed",
596
+ )
597
+
598
+ return report
599
+
600
+ # -- cleanup ------------------------------------------------------------
601
+
602
+ async def shutdown(self) -> None:
603
+ """Graceful shutdown — clear all session data."""
604
+ self._store.clear_all()
605
+ logger.info("Orchestrator: shutdown complete")