novacode-cli 1.2.0__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 (691) hide show
  1. novacode_cli/__init__.py +29 -0
  2. novacode_cli/__main__.py +6 -0
  3. novacode_cli/_lazy_heavy.py +99 -0
  4. novacode_cli/_version.py +3 -0
  5. novacode_cli/agent_stream.py +178 -0
  6. novacode_cli/agents/agent_file.py +250 -0
  7. novacode_cli/agents/agent_mailbox.py +120 -0
  8. novacode_cli/agents/approval_decisions.py +146 -0
  9. novacode_cli/agents/arg_repair.py +196 -0
  10. novacode_cli/agents/async_agents/__init__.py +6 -0
  11. novacode_cli/agents/async_agents/_config.py +213 -0
  12. novacode_cli/agents/async_agents/_mcp_tools.py +192 -0
  13. novacode_cli/agents/async_agents/_specialist.py +84 -0
  14. novacode_cli/agents/async_agents/_test_commands.py +89 -0
  15. novacode_cli/agents/async_agents/_tools.py +432 -0
  16. novacode_cli/agents/async_agents/general_purpose.py +42 -0
  17. novacode_cli/agents/async_agents/langgraph.json +9 -0
  18. novacode_cli/agents/async_agents/research.py +22 -0
  19. novacode_cli/agents/async_context.py +303 -0
  20. novacode_cli/agents/async_on_demand.py +52 -0
  21. novacode_cli/agents/async_task_errors.py +75 -0
  22. novacode_cli/agents/async_workspace.py +27 -0
  23. novacode_cli/agents/attribution.py +165 -0
  24. novacode_cli/agents/browser_mode.py +119 -0
  25. novacode_cli/agents/btw_agent.py +71 -0
  26. novacode_cli/agents/caveman.py +352 -0
  27. novacode_cli/agents/caveman_support.py +370 -0
  28. novacode_cli/agents/code_mode.py +774 -0
  29. novacode_cli/agents/code_runtime.py +160 -0
  30. novacode_cli/agents/context_compaction.py +1339 -0
  31. novacode_cli/agents/core_agent.py +2710 -0
  32. novacode_cli/agents/default_subagents/__init__.py +24 -0
  33. novacode_cli/agents/default_subagents/async_subagents.py +296 -0
  34. novacode_cli/agents/default_subagents/builtin/browser-automation-agent.md +110 -0
  35. novacode_cli/agents/default_subagents/builtin/bug-fix-agent.md +22 -0
  36. novacode_cli/agents/default_subagents/builtin/bug-investigation-agent.md +42 -0
  37. novacode_cli/agents/default_subagents/builtin/code-explorer.md +37 -0
  38. novacode_cli/agents/default_subagents/builtin/code-review-agent.md +33 -0
  39. novacode_cli/agents/default_subagents/builtin/dependency-audit-agent.md +34 -0
  40. novacode_cli/agents/default_subagents/builtin/documentation-update-agent.md +46 -0
  41. novacode_cli/agents/default_subagents/builtin/performance-audit-agent.md +41 -0
  42. novacode_cli/agents/default_subagents/builtin/plan-scout-agent.md +64 -0
  43. novacode_cli/agents/default_subagents/builtin/refactoring-agent.md +35 -0
  44. novacode_cli/agents/default_subagents/builtin/refactoring-specialist-agent.md +175 -0
  45. novacode_cli/agents/default_subagents/builtin/security-audit-agent.md +96 -0
  46. novacode_cli/agents/default_subagents/builtin/test-generation-agent.md +33 -0
  47. novacode_cli/agents/default_subagents/builtin/test-runner-agent.md +55 -0
  48. novacode_cli/agents/default_subagents/prompt.py +88 -0
  49. novacode_cli/agents/default_subagents/subagents.py +138 -0
  50. novacode_cli/agents/dream_agent.py +93 -0
  51. novacode_cli/agents/edit_diagnostics.py +362 -0
  52. novacode_cli/agents/edit_fallback.py +62 -0
  53. novacode_cli/agents/handoff.py +166 -0
  54. novacode_cli/agents/invocation.py +152 -0
  55. novacode_cli/agents/model_budget.py +42 -0
  56. novacode_cli/agents/model_retry.py +113 -0
  57. novacode_cli/agents/model_router.py +420 -0
  58. novacode_cli/agents/openai_decisions.py +186 -0
  59. novacode_cli/agents/plan_agent/__init__.py +12 -0
  60. novacode_cli/agents/plan_agent/plan_agent.py +357 -0
  61. novacode_cli/agents/plan_agent/plan_mode_middleware.py +64 -0
  62. novacode_cli/agents/plan_execution.py +237 -0
  63. novacode_cli/agents/ponytail.py +66 -0
  64. novacode_cli/agents/prime.py +274 -0
  65. novacode_cli/agents/provider_caching.py +99 -0
  66. novacode_cli/agents/server_launcher.py +904 -0
  67. novacode_cli/agents/strict_tools.py +384 -0
  68. novacode_cli/agents/task_discipline.py +294 -0
  69. novacode_cli/agents/tool_call_repair.py +290 -0
  70. novacode_cli/agents/tool_offload.py +291 -0
  71. novacode_cli/agents/tool_search.py +806 -0
  72. novacode_cli/agents/tool_search_variants.py +106 -0
  73. novacode_cli/agents/tool_verdicts.py +1194 -0
  74. novacode_cli/agents/user_async_agents.py +312 -0
  75. novacode_cli/artifacts/__init__.py +10 -0
  76. novacode_cli/artifacts/registry.py +278 -0
  77. novacode_cli/artifacts/server.py +426 -0
  78. novacode_cli/assets/Nova.png +0 -0
  79. novacode_cli/assets/nova-avatar.png +0 -0
  80. novacode_cli/audio/__init__.py +301 -0
  81. novacode_cli/audio/capture.py +137 -0
  82. novacode_cli/audio/downloads.py +273 -0
  83. novacode_cli/audio/pipeline.py +391 -0
  84. novacode_cli/audio/playback.py +87 -0
  85. novacode_cli/audio/providers.py +208 -0
  86. novacode_cli/audio/speakable.py +78 -0
  87. novacode_cli/audio/stt.py +132 -0
  88. novacode_cli/audio/stt_deepgram.py +69 -0
  89. novacode_cli/audio/stt_elevenlabs.py +89 -0
  90. novacode_cli/audio/stt_parakeet.py +94 -0
  91. novacode_cli/audio/stt_whistle.py +61 -0
  92. novacode_cli/audio/summarize.py +92 -0
  93. novacode_cli/audio/tts.py +85 -0
  94. novacode_cli/audio/tts_kokoro.py +73 -0
  95. novacode_cli/audio/tts_orpheus.py +180 -0
  96. novacode_cli/audio/tts_pocket.py +131 -0
  97. novacode_cli/audio/tts_sano.py +69 -0
  98. novacode_cli/audio/vad.py +145 -0
  99. novacode_cli/backends/__init__.py +17 -0
  100. novacode_cli/backends/filesystem.py +894 -0
  101. novacode_cli/backends/native_search.py +92 -0
  102. novacode_cli/bootstrap/__init__.py +9 -0
  103. novacode_cli/bootstrap/env_snapshot.py +240 -0
  104. novacode_cli/bootstrap/steering.py +464 -0
  105. novacode_cli/bootstrap/vision_router.py +604 -0
  106. novacode_cli/brand.py +362 -0
  107. novacode_cli/cli_args.py +718 -0
  108. novacode_cli/commands/__init__.py +237 -0
  109. novacode_cli/commands/agents_commands.py +231 -0
  110. novacode_cli/commands/auth_handler.py +40 -0
  111. novacode_cli/commands/browser_use_handler.py +75 -0
  112. novacode_cli/commands/cache_handler.py +32 -0
  113. novacode_cli/commands/chat_handler.py +1357 -0
  114. novacode_cli/commands/ci_fix.py +171 -0
  115. novacode_cli/commands/claude_plan.py +331 -0
  116. novacode_cli/commands/codex_plan.py +195 -0
  117. novacode_cli/commands/commands.py +1113 -0
  118. novacode_cli/commands/create_handler.py +124 -0
  119. novacode_cli/commands/create_server.py +1887 -0
  120. novacode_cli/commands/cron_handler.py +119 -0
  121. novacode_cli/commands/dream_handler.py +203 -0
  122. novacode_cli/commands/effort_handler.py +65 -0
  123. novacode_cli/commands/evolution_handler.py +214 -0
  124. novacode_cli/commands/file_commands.py +381 -0
  125. novacode_cli/commands/handoff_handler.py +68 -0
  126. novacode_cli/commands/hooks_handler.py +269 -0
  127. novacode_cli/commands/init_handler.py +146 -0
  128. novacode_cli/commands/learning_handler.py +160 -0
  129. novacode_cli/commands/lite_mode_handler.py +76 -0
  130. novacode_cli/commands/log_commands.py +363 -0
  131. novacode_cli/commands/mcp_handler.py +29 -0
  132. novacode_cli/commands/model_handler.py +29 -0
  133. novacode_cli/commands/notifications_handler.py +158 -0
  134. novacode_cli/commands/onboard_handler.py +121 -0
  135. novacode_cli/commands/plan_handler.py +440 -0
  136. novacode_cli/commands/planner_usage.py +250 -0
  137. novacode_cli/commands/plugin_install_handler.py +38 -0
  138. novacode_cli/commands/plugins_handler.py +35 -0
  139. novacode_cli/commands/prime_handler.py +455 -0
  140. novacode_cli/commands/prompt_handler.py +149 -0
  141. novacode_cli/commands/provider_usage.py +160 -0
  142. novacode_cli/commands/ralph_events.py +106 -0
  143. novacode_cli/commands/ralph_handler.py +1293 -0
  144. novacode_cli/commands/refine_handler.py +213 -0
  145. novacode_cli/commands/research_handler.py +202 -0
  146. novacode_cli/commands/rootapp_remote.py +83 -0
  147. novacode_cli/commands/router_handler.py +36 -0
  148. novacode_cli/commands/server_commands.py +249 -0
  149. novacode_cli/commands/session_commands.py +203 -0
  150. novacode_cli/commands/share_handler.py +450 -0
  151. novacode_cli/commands/side_commands.py +366 -0
  152. novacode_cli/commands/skill_invoke.py +420 -0
  153. novacode_cli/commands/skills_commands.py +48 -0
  154. novacode_cli/commands/trace_handler.py +322 -0
  155. novacode_cli/commands/trello_handler.py +226 -0
  156. novacode_cli/commands/trello_remote.py +177 -0
  157. novacode_cli/commands/trello_server.py +1214 -0
  158. novacode_cli/commands/vision_handler.py +127 -0
  159. novacode_cli/commands/voice_handler.py +447 -0
  160. novacode_cli/commands/webhook_handler.py +134 -0
  161. novacode_cli/commands/wiki_commands.py +227 -0
  162. novacode_cli/compaction.py +753 -0
  163. novacode_cli/computation_cache.py +159 -0
  164. novacode_cli/computer/__init__.py +1 -0
  165. novacode_cli/computer/persistent.py +234 -0
  166. novacode_cli/computer/preview.py +253 -0
  167. novacode_cli/computer/runtime.py +474 -0
  168. novacode_cli/computer/windows_console.py +152 -0
  169. novacode_cli/computer/windows_qemu.py +211 -0
  170. novacode_cli/config/browser_mode.py +159 -0
  171. novacode_cli/config/catalog_models.py +95 -0
  172. novacode_cli/config/config.py +1388 -0
  173. novacode_cli/config/credentials.py +540 -0
  174. novacode_cli/config/google_oauth_auth.py +281 -0
  175. novacode_cli/config/model_capabilities.py +343 -0
  176. novacode_cli/config/model_catalog.py +275 -0
  177. novacode_cli/config/model_create.py +1325 -0
  178. novacode_cli/config/model_manager.py +695 -0
  179. novacode_cli/config/nova_config.py +1283 -0
  180. novacode_cli/config/openai_chatgpt_auth.py +384 -0
  181. novacode_cli/config/opencode_gateway.py +25 -0
  182. novacode_cli/config/plan_mode.py +188 -0
  183. novacode_cli/config/provider_auth.py +438 -0
  184. novacode_cli/config/provider_catalog.json +5127 -0
  185. novacode_cli/config/provider_registry.py +38 -0
  186. novacode_cli/config/provider_transports.py +258 -0
  187. novacode_cli/config/role_models.py +175 -0
  188. novacode_cli/context/__init__.py +50 -0
  189. novacode_cli/context/_analysis.py +799 -0
  190. novacode_cli/context/_budget.py +116 -0
  191. novacode_cli/context/_conversation.py +73 -0
  192. novacode_cli/context/_dynamic.py +264 -0
  193. novacode_cli/context/_model_config.py +380 -0
  194. novacode_cli/context/_models_dev.py +99 -0
  195. novacode_cli/context/archives.py +248 -0
  196. novacode_cli/context/estimates.py +65 -0
  197. novacode_cli/context/history.py +17 -0
  198. novacode_cli/context/knowledge_cache.py +292 -0
  199. novacode_cli/context/manager.py +101 -0
  200. novacode_cli/context/observations.py +65 -0
  201. novacode_cli/context/pressure.py +168 -0
  202. novacode_cli/context/provider_accounting.py +109 -0
  203. novacode_cli/context/snapshot.py +493 -0
  204. novacode_cli/context/strategies.py +279 -0
  205. novacode_cli/context/task_progress.py +122 -0
  206. novacode_cli/core/__init__.py +0 -0
  207. novacode_cli/core/agent_loop.py +1821 -0
  208. novacode_cli/core/autonomous_loop.py +277 -0
  209. novacode_cli/core/code_approvals.py +57 -0
  210. novacode_cli/core/input_preparation.py +276 -0
  211. novacode_cli/core/reasoning.py +67 -0
  212. novacode_cli/core/streaming.py +326 -0
  213. novacode_cli/core/subagent_tasks.py +696 -0
  214. novacode_cli/core/subagent_tracking.py +235 -0
  215. novacode_cli/core/tool_results.py +116 -0
  216. novacode_cli/core/verification_loop.py +272 -0
  217. novacode_cli/council.py +595 -0
  218. novacode_cli/council_consensus.py +115 -0
  219. novacode_cli/council_decision.py +157 -0
  220. novacode_cli/council_planner.py +1421 -0
  221. novacode_cli/council_settings.py +208 -0
  222. novacode_cli/council_web_settings.py +41 -0
  223. novacode_cli/council_workspace.py +56 -0
  224. novacode_cli/cowork/__init__.py +10 -0
  225. novacode_cli/cowork/broker_middleware.py +162 -0
  226. novacode_cli/cowork/launcher.py +102 -0
  227. novacode_cli/cowork/policy.py +284 -0
  228. novacode_cli/cowork/ui.py +826 -0
  229. novacode_cli/daemons/__init__.py +1 -0
  230. novacode_cli/daemons/registry.py +342 -0
  231. novacode_cli/desktop_notifications.py +302 -0
  232. novacode_cli/doctor.py +251 -0
  233. novacode_cli/embeddings.py +109 -0
  234. novacode_cli/entrypoint.py +56 -0
  235. novacode_cli/errors/__init__.py +19 -0
  236. novacode_cli/errors/handlers.py +648 -0
  237. novacode_cli/errors/provider_errors.py +249 -0
  238. novacode_cli/errors/taxonomy.py +80 -0
  239. novacode_cli/events.py +179 -0
  240. novacode_cli/file_ops.py +714 -0
  241. novacode_cli/git_safety.py +281 -0
  242. novacode_cli/handoff.py +383 -0
  243. novacode_cli/headless/__init__.py +18 -0
  244. novacode_cli/headless/input.py +58 -0
  245. novacode_cli/headless/output.py +224 -0
  246. novacode_cli/headless/pipe.py +532 -0
  247. novacode_cli/headless/runner.py +461 -0
  248. novacode_cli/hermes/__init__.py +39 -0
  249. novacode_cli/hermes/activation.py +297 -0
  250. novacode_cli/hermes/config.py +129 -0
  251. novacode_cli/hermes/curator.py +199 -0
  252. novacode_cli/hermes/evolution.py +374 -0
  253. novacode_cli/hermes/ledger.py +364 -0
  254. novacode_cli/hermes/memory_tiers.py +969 -0
  255. novacode_cli/hermes/middleware.py +678 -0
  256. novacode_cli/hermes/overview.py +174 -0
  257. novacode_cli/hermes/prompt_evolution.py +503 -0
  258. novacode_cli/hermes/refine_loop.py +643 -0
  259. novacode_cli/hermes/refinement_log.py +190 -0
  260. novacode_cli/hermes/review.py +674 -0
  261. novacode_cli/hermes/skill_debate.py +135 -0
  262. novacode_cli/hermes/skill_discovery.py +450 -0
  263. novacode_cli/hermes/skill_manager.py +335 -0
  264. novacode_cli/hermes/skill_proposals.py +43 -0
  265. novacode_cli/hermes/test_gaming.py +119 -0
  266. novacode_cli/hermes/tracker.py +435 -0
  267. novacode_cli/hermes/tuner.py +206 -0
  268. novacode_cli/hermes/verification_memory.py +134 -0
  269. novacode_cli/hermes/verifier.py +277 -0
  270. novacode_cli/hitl/__init__.py +22 -0
  271. novacode_cli/hitl/interrupts.py +493 -0
  272. novacode_cli/hooks.py +359 -0
  273. novacode_cli/image_utils.py +517 -0
  274. novacode_cli/init/__init__.py +7 -0
  275. novacode_cli/init/events.py +43 -0
  276. novacode_cli/input_utils.py +526 -0
  277. novacode_cli/install_commands.py +87 -0
  278. novacode_cli/integrations/__init__.py +82 -0
  279. novacode_cli/integrations/daytona.py +122 -0
  280. novacode_cli/integrations/docker.py +309 -0
  281. novacode_cli/integrations/host_path.py +111 -0
  282. novacode_cli/integrations/langsmith.py +151 -0
  283. novacode_cli/integrations/modal.py +134 -0
  284. novacode_cli/integrations/runloop.py +130 -0
  285. novacode_cli/integrations/sandbox_factory.py +1388 -0
  286. novacode_cli/integrations/sandbox_registry.py +312 -0
  287. novacode_cli/integrations/workdir_backend.py +589 -0
  288. novacode_cli/main.py +2078 -0
  289. novacode_cli/mcp/__init__.py +73 -0
  290. novacode_cli/mcp/auth.py +189 -0
  291. novacode_cli/mcp/client.py +571 -0
  292. novacode_cli/mcp/code_mode.py +5 -0
  293. novacode_cli/mcp/code_runtime.py +5 -0
  294. novacode_cli/mcp/commands.py +902 -0
  295. novacode_cli/mcp/config.py +406 -0
  296. novacode_cli/mcp/generic.py +128 -0
  297. novacode_cli/mcp/middleware.py +993 -0
  298. novacode_cli/mcp/presets.py +367 -0
  299. novacode_cli/memory/__init__.py +1 -0
  300. novacode_cli/memory/agent_memory.py +1312 -0
  301. novacode_cli/memory/builtin_lessons.py +60 -0
  302. novacode_cli/memory/dream_gates.py +192 -0
  303. novacode_cli/memory/limits.py +59 -0
  304. novacode_cli/memory/parsed_cache.py +106 -0
  305. novacode_cli/memory/recall_stats.py +97 -0
  306. novacode_cli/memory/semantic.py +116 -0
  307. novacode_cli/memory/store.py +335 -0
  308. novacode_cli/memory/wiki.py +274 -0
  309. novacode_cli/migrate.py +185 -0
  310. novacode_cli/onboarding.py +649 -0
  311. novacode_cli/onboarding_check.py +119 -0
  312. novacode_cli/path_approval.py +297 -0
  313. novacode_cli/plan_archive.py +148 -0
  314. novacode_cli/plugins/__init__.py +121 -0
  315. novacode_cli/plugins/claude_plugins.py +538 -0
  316. novacode_cli/plugins/cli.py +168 -0
  317. novacode_cli/plugins/components.py +413 -0
  318. novacode_cli/plugins/hooks.py +356 -0
  319. novacode_cli/plugins/loader.py +317 -0
  320. novacode_cli/plugins/marketplaces.py +199 -0
  321. novacode_cli/plugins/storage.py +70 -0
  322. novacode_cli/prime.py +759 -0
  323. novacode_cli/prime_factory/__init__.py +15 -0
  324. novacode_cli/prime_factory/container.py +198 -0
  325. novacode_cli/prime_factory/controller.py +565 -0
  326. novacode_cli/prime_factory/knowledge.py +261 -0
  327. novacode_cli/prime_factory/publication.py +218 -0
  328. novacode_cli/prime_factory/recovery.py +50 -0
  329. novacode_cli/prime_factory/research.py +370 -0
  330. novacode_cli/prime_factory/runner.py +608 -0
  331. novacode_cli/prime_factory/runtime.py +261 -0
  332. novacode_cli/prime_factory/state.py +201 -0
  333. novacode_cli/prime_factory/store.py +128 -0
  334. novacode_cli/prime_factory/worker.py +129 -0
  335. novacode_cli/prime_factory/workflow.py +899 -0
  336. novacode_cli/prime_factory/workspace.py +317 -0
  337. novacode_cli/prime_memory.py +290 -0
  338. novacode_cli/process_manager.py +585 -0
  339. novacode_cli/project_onboard.py +537 -0
  340. novacode_cli/prompts/System_Prompt_Nova.jinja +23 -0
  341. novacode_cli/prompts/__init__.py +168 -0
  342. novacode_cli/prompts/agent_definitions.jinja +11 -0
  343. novacode_cli/prompts/agent_generation.jinja +125 -0
  344. novacode_cli/prompts/async_agents/general_purpose.jinja +22 -0
  345. novacode_cli/prompts/claude_plan_mode.jinja +12 -0
  346. novacode_cli/prompts/coding_workflow.jinja +18 -0
  347. novacode_cli/prompts/computer_use.jinja +17 -0
  348. novacode_cli/prompts/core_agent_system.jinja +81 -0
  349. novacode_cli/prompts/delegation.jinja +15 -0
  350. novacode_cli/prompts/desktop_power.jinja +4 -0
  351. novacode_cli/prompts/file_paths.jinja +15 -0
  352. novacode_cli/prompts/file_tracker.jinja +23 -0
  353. novacode_cli/prompts/init_agents_md.jinja +56 -0
  354. novacode_cli/prompts/init_exploration.jinja +174 -0
  355. novacode_cli/prompts/init_semantic_orchestrate.jinja +37 -0
  356. novacode_cli/prompts/instruction_scope.jinja +5 -0
  357. novacode_cli/prompts/learnings_extraction.jinja +45 -0
  358. novacode_cli/prompts/longterm_memory.jinja +54 -0
  359. novacode_cli/prompts/mcp.jinja +84 -0
  360. novacode_cli/prompts/memory_consolidation.jinja +40 -0
  361. novacode_cli/prompts/nova_evolution.jinja +73 -0
  362. novacode_cli/prompts/nova_review.jinja +158 -0
  363. novacode_cli/prompts/nova_skill_debate.jinja +36 -0
  364. novacode_cli/prompts/nova_standing_instructions.jinja +26 -0
  365. novacode_cli/prompts/nova_verify.jinja +100 -0
  366. novacode_cli/prompts/onboard_dashboard.jinja +378 -0
  367. novacode_cli/prompts/plan_agent.jinja +87 -0
  368. novacode_cli/prompts/plan_scouts.jinja +5 -0
  369. novacode_cli/prompts/plan_storage.jinja +8 -0
  370. novacode_cli/prompts/planning.jinja +82 -0
  371. novacode_cli/prompts/ponytail/LICENSE +21 -0
  372. novacode_cli/prompts/ponytail/UPSTREAM.json +15 -0
  373. novacode_cli/prompts/ponytail/hooks/ponytail-config.js +175 -0
  374. novacode_cli/prompts/ponytail/hooks/ponytail-instructions.js +98 -0
  375. novacode_cli/prompts/ponytail/skills/ponytail/SKILL.md +52 -0
  376. novacode_cli/prompts/ponytail/skills/ponytail-audit/SKILL.md +103 -0
  377. novacode_cli/prompts/ponytail/skills/ponytail-debt/SKILL.md +44 -0
  378. novacode_cli/prompts/ponytail/skills/ponytail-gain/SKILL.md +52 -0
  379. novacode_cli/prompts/ponytail/skills/ponytail-help/SKILL.md +76 -0
  380. novacode_cli/prompts/ponytail/skills/ponytail-review/SKILL.md +101 -0
  381. novacode_cli/prompts/prompt_evolution.jinja +27 -0
  382. novacode_cli/prompts/ralph_iteration.jinja +75 -0
  383. novacode_cli/prompts/refine_gate.jinja +31 -0
  384. novacode_cli/prompts/refine_plan.jinja +64 -0
  385. novacode_cli/prompts/refine_review.jinja +34 -0
  386. novacode_cli/prompts/research_delivery.jinja +9 -0
  387. novacode_cli/prompts/research_swarm.jinja +130 -0
  388. novacode_cli/prompts/response_style.jinja +5 -0
  389. novacode_cli/prompts/session_summarization.jinja +86 -0
  390. novacode_cli/prompts/skill_creation.jinja +85 -0
  391. novacode_cli/prompts/subagents/fact_checker.jinja +48 -0
  392. novacode_cli/prompts/subagents/financial_analyst.jinja +50 -0
  393. novacode_cli/prompts/subagents/literature_reviewer.jinja +52 -0
  394. novacode_cli/prompts/subagents/market_analyst.jinja +48 -0
  395. novacode_cli/prompts/subagents/research_synthesizer.jinja +57 -0
  396. novacode_cli/prompts/subagents/technical_researcher.jinja +56 -0
  397. novacode_cli/prompts/subagents/web_researcher.jinja +46 -0
  398. novacode_cli/prompts/summarization.jinja +107 -0
  399. novacode_cli/prompts/summarization_answers.jinja +23 -0
  400. novacode_cli/prompts/summarization_questions.jinja +32 -0
  401. novacode_cli/prompts/vision_caption.jinja +21 -0
  402. novacode_cli/prompts/voice_summary.jinja +65 -0
  403. novacode_cli/prompts/wiki_file.jinja +57 -0
  404. novacode_cli/prompts/wiki_ingest.jinja +87 -0
  405. novacode_cli/recovery.py +383 -0
  406. novacode_cli/remote/__init__.py +45 -0
  407. novacode_cli/remote/approvals.py +282 -0
  408. novacode_cli/remote/async_task_clients.py +97 -0
  409. novacode_cli/remote/async_task_watcher.py +240 -0
  410. novacode_cli/remote/bridge.py +905 -0
  411. novacode_cli/remote/command_menu.py +26 -0
  412. novacode_cli/remote/command_ui.py +243 -0
  413. novacode_cli/remote/config.py +199 -0
  414. novacode_cli/remote/discord_bridge.py +530 -0
  415. novacode_cli/remote/discord_format.py +31 -0
  416. novacode_cli/remote/discord_threads.py +225 -0
  417. novacode_cli/remote/images.py +97 -0
  418. novacode_cli/remote/processor.py +601 -0
  419. novacode_cli/remote/rootapp_bot/bot.cjs +16 -0
  420. novacode_cli/remote/rootapp_bot/package-lock.json +2683 -0
  421. novacode_cli/remote/rootapp_bot/package.json +18 -0
  422. novacode_cli/remote/rootapp_bot/transport.cjs +80 -0
  423. novacode_cli/remote/rootapp_bridge.py +370 -0
  424. novacode_cli/remote/rootapp_setup.py +89 -0
  425. novacode_cli/remote/routing.py +103 -0
  426. novacode_cli/remote/scheduler.py +302 -0
  427. novacode_cli/remote/session.py +164 -0
  428. novacode_cli/remote/status.py +411 -0
  429. novacode_cli/remote/streaming.py +188 -0
  430. novacode_cli/remote/telegram_bridge.py +1118 -0
  431. novacode_cli/remote/telegram_format.py +215 -0
  432. novacode_cli/remote/telegram_hub.py +256 -0
  433. novacode_cli/remote/voice_notes.py +120 -0
  434. novacode_cli/remote/webhook_adapters.py +167 -0
  435. novacode_cli/remote/webhook_server.py +154 -0
  436. novacode_cli/rewind.py +150 -0
  437. novacode_cli/rooms/__init__.py +1 -0
  438. novacode_cli/rooms/bridge.py +86 -0
  439. novacode_cli/rooms/cli.py +231 -0
  440. novacode_cli/rooms/nova.py +227 -0
  441. novacode_cli/rooms/runtime.py +299 -0
  442. novacode_cli/rooms/store.py +538 -0
  443. novacode_cli/rooms/workspace.py +94 -0
  444. novacode_cli/security/__init__.py +29 -0
  445. novacode_cli/security/approval_context.py +45 -0
  446. novacode_cli/security/delegated_approval.py +68 -0
  447. novacode_cli/security/local_http.py +85 -0
  448. novacode_cli/security/middleware.py +87 -0
  449. novacode_cli/security/paths.py +35 -0
  450. novacode_cli/security/policy.py +535 -0
  451. novacode_cli/security/policy_writer.py +90 -0
  452. novacode_cli/security/prompt_injection.py +203 -0
  453. novacode_cli/security/remember.py +81 -0
  454. novacode_cli/security/rule_synthesis.py +116 -0
  455. novacode_cli/security/secret_files.py +61 -0
  456. novacode_cli/security/session_allow.py +114 -0
  457. novacode_cli/security/unicode_security.py +516 -0
  458. novacode_cli/security/validator.py +173 -0
  459. novacode_cli/server/__init__.py +9 -0
  460. novacode_cli/server/app.py +632 -0
  461. novacode_cli/server/event_adapter.py +189 -0
  462. novacode_cli/server/session_manager.py +90 -0
  463. novacode_cli/server_main.py +64 -0
  464. novacode_cli/server_runner/dev_server.py +530 -0
  465. novacode_cli/server_runner/test_runner.py +470 -0
  466. novacode_cli/session/__init__.py +1 -0
  467. novacode_cli/session/adapters/__init__.py +240 -0
  468. novacode_cli/session/adapters/claude_code.py +102 -0
  469. novacode_cli/session/adapters/codex.py +77 -0
  470. novacode_cli/session/adapters/harness_native.py +75 -0
  471. novacode_cli/session/checkpoint_coordinator.py +448 -0
  472. novacode_cli/session/context_restore.py +25 -0
  473. novacode_cli/session/history_export.py +228 -0
  474. novacode_cli/session/history_tree.py +174 -0
  475. novacode_cli/session/imported_context.py +267 -0
  476. novacode_cli/session/runtime_identity.py +32 -0
  477. novacode_cli/session/session_persistence.py +1168 -0
  478. novacode_cli/session/session_prompt_builder.py +279 -0
  479. novacode_cli/session/session_restore.py +351 -0
  480. novacode_cli/session/session_summarization.py +20 -0
  481. novacode_cli/session/transcript_journal.py +153 -0
  482. novacode_cli/sessions/__init__.py +11 -0
  483. novacode_cli/sessions/gateway.py +84 -0
  484. novacode_cli/sessions/launch.py +51 -0
  485. novacode_cli/sessions/lease.py +213 -0
  486. novacode_cli/sessions/messaging.py +255 -0
  487. novacode_cli/sessions/protocol.py +216 -0
  488. novacode_cli/sessions/resume.py +31 -0
  489. novacode_cli/sessions/supervisor.py +498 -0
  490. novacode_cli/sessions/tasks.py +73 -0
  491. novacode_cli/sessions/tui_messaging.py +413 -0
  492. novacode_cli/sessions/worker.py +652 -0
  493. novacode_cli/sessions/worktree.py +262 -0
  494. novacode_cli/shell/__init__.py +27 -0
  495. novacode_cli/shell/jobs.py +475 -0
  496. novacode_cli/shell/middleware.py +1568 -0
  497. novacode_cli/shell/os_sandbox.py +205 -0
  498. novacode_cli/shell/output.py +181 -0
  499. novacode_cli/shell/patterns.py +275 -0
  500. novacode_cli/shell/processes.py +87 -0
  501. novacode_cli/shell/utils.py +236 -0
  502. novacode_cli/skills/__init__.py +26 -0
  503. novacode_cli/skills/builtin/handoff/SKILL.md +15 -0
  504. novacode_cli/skills/curation_middleware.py +69 -0
  505. novacode_cli/skills/executable.py +161 -0
  506. novacode_cli/skills/libraries.py +102 -0
  507. novacode_cli/skills/load.py +248 -0
  508. novacode_cli/skills/prune.py +215 -0
  509. novacode_cli/skills/refreshing_middleware.py +896 -0
  510. novacode_cli/skills/retrieval.py +248 -0
  511. novacode_cli/skills/runtime.py +118 -0
  512. novacode_cli/skills/schema.py +254 -0
  513. novacode_cli/skills/skill_creation.py +1549 -0
  514. novacode_cli/skills/skill_lock.py +75 -0
  515. novacode_cli/skills/skill_system_prompt.py +16 -0
  516. novacode_cli/skills/skills_prefs.py +126 -0
  517. novacode_cli/skills/tool_resolver.py +31 -0
  518. novacode_cli/skills/upstream.py +50 -0
  519. novacode_cli/skills/versioning.py +291 -0
  520. novacode_cli/states/Session.py +847 -0
  521. novacode_cli/states/__init__.py +31 -0
  522. novacode_cli/states/slices/__init__.py +17 -0
  523. novacode_cli/states/slices/agent_runtime.py +249 -0
  524. novacode_cli/states/slices/background_tasks.py +21 -0
  525. novacode_cli/states/slices/notifications.py +74 -0
  526. novacode_cli/states/slices/remote_bridge.py +56 -0
  527. novacode_cli/states/slices/ui_settings.py +49 -0
  528. novacode_cli/states/slices/wiki.py +25 -0
  529. novacode_cli/teammates/__init__.py +1 -0
  530. novacode_cli/teammates/controller.py +557 -0
  531. novacode_cli/teammates/graph.py +104 -0
  532. novacode_cli/teammates/registry.py +175 -0
  533. novacode_cli/teammates/service.py +254 -0
  534. novacode_cli/teammates/worker.py +19 -0
  535. novacode_cli/teams/__init__.py +1 -0
  536. novacode_cli/teams/adapters/__init__.py +1 -0
  537. novacode_cli/teams/adapters/container.py +316 -0
  538. novacode_cli/teams/adapters/inprocess.py +206 -0
  539. novacode_cli/teams/adapters/process.py +364 -0
  540. novacode_cli/teams/bootstrap.py +246 -0
  541. novacode_cli/teams/container_git.py +67 -0
  542. novacode_cli/teams/context.py +34 -0
  543. novacode_cli/teams/coordinator.py +815 -0
  544. novacode_cli/teams/gateway.py +200 -0
  545. novacode_cli/teams/host.py +64 -0
  546. novacode_cli/teams/legacy.py +52 -0
  547. novacode_cli/teams/middleware.py +176 -0
  548. novacode_cli/teams/models.py +104 -0
  549. novacode_cli/teams/output.py +47 -0
  550. novacode_cli/teams/protocol.py +50 -0
  551. novacode_cli/teams/recovery.py +80 -0
  552. novacode_cli/teams/scheduler.py +44 -0
  553. novacode_cli/teams/store.py +631 -0
  554. novacode_cli/teams/worker.py +251 -0
  555. novacode_cli/teams/workspace.py +100 -0
  556. novacode_cli/token_utils.py +256 -0
  557. novacode_cli/tools/__init__.py +104 -0
  558. novacode_cli/tools/_shared.py +97 -0
  559. novacode_cli/tools/_types.py +24 -0
  560. novacode_cli/tools/artifact_tools.py +137 -0
  561. novacode_cli/tools/code_search_tools.py +330 -0
  562. novacode_cli/tools/computer_tools.py +77 -0
  563. novacode_cli/tools/cron_tools.py +65 -0
  564. novacode_cli/tools/daemon_tool.py +290 -0
  565. novacode_cli/tools/desktop_tools.py +161 -0
  566. novacode_cli/tools/fetch_tools.py +530 -0
  567. novacode_cli/tools/http_tools.py +279 -0
  568. novacode_cli/tools/inspection_tools.py +107 -0
  569. novacode_cli/tools/job_tools.py +143 -0
  570. novacode_cli/tools/knowledge_tools.py +427 -0
  571. novacode_cli/tools/memory_tools.py +461 -0
  572. novacode_cli/tools/memory_wiki_tools.py +40 -0
  573. novacode_cli/tools/notebook_tools.py +238 -0
  574. novacode_cli/tools/oracle_tool.py +83 -0
  575. novacode_cli/tools/plan_archive_tools.py +80 -0
  576. novacode_cli/tools/plan_mode_tools.py +315 -0
  577. novacode_cli/tools/python_kernel_loop.py +165 -0
  578. novacode_cli/tools/python_kernel_tool.py +222 -0
  579. novacode_cli/tools/reflection_tools.py +34 -0
  580. novacode_cli/tools/scraper_tools.py +377 -0
  581. novacode_cli/tools/skill_tools.py +275 -0
  582. novacode_cli/tools/speak_tool.py +51 -0
  583. novacode_cli/tools/store_memory_tools.py +227 -0
  584. novacode_cli/tools/ui_tools.py +62 -0
  585. novacode_cli/tools/waiter_tools.py +408 -0
  586. novacode_cli/tools/web_tools.py +349 -0
  587. novacode_cli/tools/wiki_tools.py +152 -0
  588. novacode_cli/tools/worktree_tools.py +219 -0
  589. novacode_cli/tracking/__init__.py +10 -0
  590. novacode_cli/tracking/file_tracker.py +826 -0
  591. novacode_cli/tracking/local_metrics.py +170 -0
  592. novacode_cli/tracking/loop_guard.py +507 -0
  593. novacode_cli/tracking/request_metrics.py +60 -0
  594. novacode_cli/tracking/tracing.py +692 -0
  595. novacode_cli/tracking/usage_tree.py +677 -0
  596. novacode_cli/tracking/workspace_anchoring.py +231 -0
  597. novacode_cli/tui/__init__.py +11 -0
  598. novacode_cli/tui/animation_rate.py +9 -0
  599. novacode_cli/tui/animations.py +332 -0
  600. novacode_cli/tui/app.py +19315 -0
  601. novacode_cli/tui/auth_display.py +87 -0
  602. novacode_cli/tui/auth_screens.py +826 -0
  603. novacode_cli/tui/avatar.py +294 -0
  604. novacode_cli/tui/avatar_frames.py +21 -0
  605. novacode_cli/tui/background_tasks.py +35 -0
  606. novacode_cli/tui/browser_settings_screen.py +70 -0
  607. novacode_cli/tui/caveman_screen.py +185 -0
  608. novacode_cli/tui/council_settings_screen.py +182 -0
  609. novacode_cli/tui/council_view.py +535 -0
  610. novacode_cli/tui/decision_settings.py +268 -0
  611. novacode_cli/tui/diff_preview.py +199 -0
  612. novacode_cli/tui/gc_tuning.py +63 -0
  613. novacode_cli/tui/handoff_screen.py +206 -0
  614. novacode_cli/tui/harness.py +244 -0
  615. novacode_cli/tui/history_tree_screen.py +179 -0
  616. novacode_cli/tui/home.py +328 -0
  617. novacode_cli/tui/home_scene.py +94 -0
  618. novacode_cli/tui/icons.py +81 -0
  619. novacode_cli/tui/interface_styles.tcss +327 -0
  620. novacode_cli/tui/matrix3.py +108 -0
  621. novacode_cli/tui/mcp_tools_screen.py +131 -0
  622. novacode_cli/tui/motion.py +178 -0
  623. novacode_cli/tui/output_body.py +136 -0
  624. novacode_cli/tui/output_buffer.py +59 -0
  625. novacode_cli/tui/palette.py +334 -0
  626. novacode_cli/tui/pickers.py +237 -0
  627. novacode_cli/tui/prime_panel.py +506 -0
  628. novacode_cli/tui/provider_signin.py +213 -0
  629. novacode_cli/tui/pty_shell.py +159 -0
  630. novacode_cli/tui/reference_screens.py +444 -0
  631. novacode_cli/tui/resume_picker.py +346 -0
  632. novacode_cli/tui/rooms.py +597 -0
  633. novacode_cli/tui/screens.py +7088 -0
  634. novacode_cli/tui/session_import.py +333 -0
  635. novacode_cli/tui/session_pane.py +242 -0
  636. novacode_cli/tui/session_tab.py +42 -0
  637. novacode_cli/tui/session_transcript.py +118 -0
  638. novacode_cli/tui/shell_output.py +7 -0
  639. novacode_cli/tui/stall_watch.py +174 -0
  640. novacode_cli/tui/statusline.py +40 -0
  641. novacode_cli/tui/statusline_screen.py +91 -0
  642. novacode_cli/tui/teammate_updates.py +162 -0
  643. novacode_cli/tui/teammates.py +548 -0
  644. novacode_cli/tui/terminal.py +289 -0
  645. novacode_cli/tui/terminal_view.py +235 -0
  646. novacode_cli/tui/themes.py +118 -0
  647. novacode_cli/tui/update_notice.py +84 -0
  648. novacode_cli/tui/voice_downloads.py +113 -0
  649. novacode_cli/tui/widgets.py +2038 -0
  650. novacode_cli/tui/windows_input.py +38 -0
  651. novacode_cli/tui/workspace_approval.py +165 -0
  652. novacode_cli/ui/council_web.py +113 -0
  653. novacode_cli/ui/council_workspace.py +72 -0
  654. novacode_cli/ui/cowork_web.py +41 -0
  655. novacode_cli/ui/diff_highlight.py +160 -0
  656. novacode_cli/ui/execution.py +1396 -0
  657. novacode_cli/ui/exit_summary.py +65 -0
  658. novacode_cli/ui/hitl_approval.py +196 -0
  659. novacode_cli/ui/input_preparation.py +2 -0
  660. novacode_cli/ui/interrupt_handlers.py +215 -0
  661. novacode_cli/ui/question_prompt.py +151 -0
  662. novacode_cli/ui/status_phrases.py +157 -0
  663. novacode_cli/ui/stream_preview.py +66 -0
  664. novacode_cli/ui/streaming.py +2 -0
  665. novacode_cli/ui/subagent_tracking.py +2 -0
  666. novacode_cli/ui/ui_elements.py +1585 -0
  667. novacode_cli/ui/web_workspace.py +244 -0
  668. novacode_cli/ui_events.py +299 -0
  669. novacode_cli/update_ui.py +243 -0
  670. novacode_cli/updates.py +489 -0
  671. novacode_cli/utils/backend_patches.py +399 -0
  672. novacode_cli/utils/backend_paths.py +467 -0
  673. novacode_cli/utils/lazy_providers.py +51 -0
  674. novacode_cli/utils/model_info.py +223 -0
  675. novacode_cli/utils/pdf_extraction.py +183 -0
  676. novacode_cli/utils/startup.py +29 -0
  677. novacode_cli/utils/tasks.py +42 -0
  678. novacode_cli/utils/windows.py +9 -0
  679. novacode_cli/vixie/hook_adapter.py +127 -0
  680. novacode_cli/vixie/server.py +375 -0
  681. novacode_cli/wiki/__init__.py +15 -0
  682. novacode_cli/wiki/ask.py +56 -0
  683. novacode_cli/wiki/file.py +71 -0
  684. novacode_cli/wiki/ingest.py +184 -0
  685. novacode_cli/wiki/manager.py +261 -0
  686. novacode_cli-1.2.0.dist-info/METADATA +271 -0
  687. novacode_cli-1.2.0.dist-info/RECORD +691 -0
  688. novacode_cli-1.2.0.dist-info/WHEEL +5 -0
  689. novacode_cli-1.2.0.dist-info/entry_points.txt +2 -0
  690. novacode_cli-1.2.0.dist-info/licenses/LICENSE +21 -0
  691. novacode_cli-1.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,595 @@
1
+ """Council of agents — independent answers + democratic voting.
2
+
3
+ A *council* takes a single question and runs it past five distinct personas,
4
+ each backed by the session's configured model but given its own personality.
5
+ Every member answers **independently** (it cannot see the others' answers), then
6
+ each casts a **single democratic vote** for the best answer (never its own). The
7
+ **majority** answer is the verdict.
8
+
9
+ :func:`run_council` is a pure async generator yielding plain ``dict`` events, so
10
+ it can be unit-tested without HTTP and rendered by the ``/council`` web server as
11
+ Server-Sent Events (see :mod:`novacode_cli.commands.chat_handler`).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ import json
18
+ import logging
19
+ import re
20
+ from dataclasses import dataclass
21
+ from typing import TYPE_CHECKING, Any
22
+
23
+ if TYPE_CHECKING:
24
+ from collections.abc import AsyncIterator
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ # Max web searches a single persona may run before it must answer.
29
+ _MAX_TOOL_ROUNDS = 2
30
+
31
+ # Tool offered to each persona, as a plain OpenAI-style schema dict. Passing a
32
+ # dict (rather than a LangChain ``@tool``) keeps this module import-light — it
33
+ # avoids importing ``langchain_core`` (which pulls in transformers) at import
34
+ # time; the configured model already brings LangChain with it at run time.
35
+ _SEARCH_TOOL_NAME = "council_web_search"
36
+ _SEARCH_TOOL_SCHEMA: dict[str, Any] = {
37
+ "type": "function",
38
+ "function": {
39
+ "name": _SEARCH_TOOL_NAME,
40
+ "description": (
41
+ "Search the web (DuckDuckGo) for current, factual information. Use a "
42
+ "focused query. Call this only when you genuinely need up-to-date or "
43
+ "factual grounding for your perspective."
44
+ ),
45
+ "parameters": {
46
+ "type": "object",
47
+ "properties": {
48
+ "query": {"type": "string", "description": "The search query."}
49
+ },
50
+ "required": ["query"],
51
+ },
52
+ },
53
+ }
54
+
55
+
56
+ def _run_council_tool(name: str, args: dict[str, Any]) -> str:
57
+ """Execute a council tool call by name and return its string result."""
58
+ if name == _SEARCH_TOOL_NAME:
59
+ from novacode_cli.tools.web_tools import duckduckgo_search
60
+
61
+ try:
62
+ res = duckduckgo_search(str(args.get("query", "")), max_results=4) # type: ignore
63
+ except Exception as exc: # noqa: BLE001
64
+ return f"(search error: {exc})"
65
+ if not res.get("success"):
66
+ return f"(search failed: {res.get('error', 'unknown error')})"
67
+ results = res.get("results", [])
68
+ if not results:
69
+ return "(no results)"
70
+ return "\n".join(
71
+ f"- {r.get('title', '')}: {r.get('body', '')} ({r.get('url', '')})"
72
+ for r in results
73
+ )
74
+ return f"(unknown tool: {name})"
75
+
76
+
77
+ @dataclass(frozen=True)
78
+ class Persona:
79
+ """One council member: an id, display identity, and a system prompt."""
80
+
81
+ id: str
82
+ name: str
83
+ avatar: str
84
+ color: str
85
+ system: str
86
+
87
+
88
+ PERSONAS: list[Persona] = [
89
+ Persona(
90
+ id="architect",
91
+ name="The Architect",
92
+ avatar="🏛️",
93
+ color="#6ea8fe",
94
+ system=(
95
+ "You are The Architect. You think in systems, boundaries, and the "
96
+ "long view: structure, separation of concerns, data flow, and how a "
97
+ "choice ages over years. You value maintainability over cleverness."
98
+ ),
99
+ ),
100
+ Persona(
101
+ id="pragmatist",
102
+ name="The Pragmatist",
103
+ avatar="🛠️",
104
+ color="#4cae8a",
105
+ system=(
106
+ "You are The Pragmatist. You care about shipping working software. "
107
+ "You favor the simplest thing that solves the real problem, weigh "
108
+ "effort against payoff, and call out over-engineering and YAGNI."
109
+ ),
110
+ ),
111
+ Persona(
112
+ id="skeptic",
113
+ name="The Skeptic",
114
+ avatar="🔍",
115
+ color="#d4a13b",
116
+ system=(
117
+ "You are The Skeptic. You hunt for what breaks: edge cases, failure "
118
+ "modes, race conditions, security holes, and hidden assumptions. You "
119
+ "are constructive but you do not let risks go unnamed."
120
+ ),
121
+ ),
122
+ Persona(
123
+ id="innovator",
124
+ name="The Innovator",
125
+ avatar="🚀",
126
+ color="#b06ae8",
127
+ system=(
128
+ "You are The Innovator. You look for the non-obvious, modern, or "
129
+ "creative approach others miss. You bring fresh techniques and "
130
+ "question whether the framing of the problem is even right."
131
+ ),
132
+ ),
133
+ Persona(
134
+ id="minimalist",
135
+ name="The Minimalist",
136
+ avatar="✂️",
137
+ color="#d46a6a",
138
+ system=(
139
+ "You are The Minimalist. You strip problems to their essence and "
140
+ "prefer the least code, the fewest moving parts, and the clearest "
141
+ "solution. You delete before you add."
142
+ ),
143
+ ),
144
+ ]
145
+
146
+ _RESPONSE_GUIDE = (
147
+ "\n\nYou are one member of a council of advisors answering independently — "
148
+ "you cannot see the others' answers, so give a COMPLETE answer rather than "
149
+ "a single angle, and never refer to what anyone else said.\n"
150
+ "\n"
151
+ "Answer in this shape, without headings or preamble:\n"
152
+ "1. Your recommendation, in the first sentence. Commit to one — a reader "
153
+ "who stops there should know what you would do.\n"
154
+ "2. The reasoning that actually drives it, from your perspective.\n"
155
+ "3. The strongest case against your own recommendation, and why you still "
156
+ "hold it. If it would change your mind, name what evidence would.\n"
157
+ "\n"
158
+ "Be concrete: name the technique, the tradeoff, the failure. Prefer a "
159
+ "specific claim you might be wrong about over a safe generality. No "
160
+ "bullet-point walls, no restating the question, no 'it depends' without "
161
+ "saying what it depends on. Roughly 150-200 words. Speak in the first "
162
+ "person, in character — your perspective is why you are on this council, "
163
+ "but the answer must stand on its merits, not on your persona.\n"
164
+ "If the question turns on current or factual information you are unsure "
165
+ "of, search first, then answer."
166
+ )
167
+
168
+ _VOTE_GUIDE = (
169
+ "\n\nThe council has answered independently. Vote for the SINGLE best "
170
+ "answer on merit. Your own answer is not on the ballot.\n"
171
+ "\n"
172
+ "Judge by: does it actually answer the question asked; is the reasoning "
173
+ "sound; is it specific enough to act on; does it hold up against the risks "
174
+ "it acknowledges. Do NOT reward the answer that most resembles your own "
175
+ "outlook, length, or confidence — vote for the one you would want acted on "
176
+ "if the decision were yours and you had to live with it.\n"
177
+ "\n"
178
+ 'Respond with ONLY this JSON and nothing else: {"choice":"<exact advisor '
179
+ 'name>","reason":"<one specific sentence naming what made it best>"}'
180
+ )
181
+
182
+
183
+ # How much prior-round context to carry into a follow-up council.
184
+ _MAX_HISTORY_ROUNDS = 6
185
+ _HISTORY_ANSWER_CHARS = 400
186
+
187
+
188
+ def _format_history(history: list[dict[str, Any]] | None) -> str:
189
+ """Render prior council rounds into a compact context block for follow-ups.
190
+
191
+ Each round is ``{"topic": str, "transcript": [[name, text], ...],
192
+ "winner": str}``. Older rounds and long answers are trimmed so the prompt
193
+ stays bounded while preserving the thread of discussion.
194
+ """
195
+ if not history:
196
+ return ""
197
+ lines = ["Earlier in this council session (for context, so you can follow up):"]
198
+ for idx, rnd in enumerate(history[-_MAX_HISTORY_ROUNDS:], 1):
199
+ lines.append(f"\nRound {idx} — topic: {rnd.get('topic', '')}")
200
+ for entry in rnd.get("transcript", []):
201
+ try:
202
+ name, text = entry
203
+ except (ValueError, TypeError):
204
+ continue
205
+ text = (text or "").strip()
206
+ if len(text) > _HISTORY_ANSWER_CHARS:
207
+ text = text[:_HISTORY_ANSWER_CHARS] + "…"
208
+ lines.append(f" {name}: {text}")
209
+ if rnd.get("winner"):
210
+ lines.append(f" -> Verdict: {rnd['winner']} won that round.")
211
+ return "\n".join(lines)
212
+
213
+
214
+ def _content_text(message: Any) -> str:
215
+ """Extract plain text from a LangChain message/chunk ``.content``.
216
+
217
+ Content may be a string or a list of content blocks (e.g. Anthropic), so we
218
+ coalesce any text parts and ignore the rest.
219
+ """
220
+ content = getattr(message, "content", message)
221
+ if isinstance(content, str):
222
+ return content
223
+ if isinstance(content, list):
224
+ parts: list[str] = []
225
+ for block in content:
226
+ if isinstance(block, str):
227
+ parts.append(block)
228
+ elif isinstance(block, dict):
229
+ text = block.get("text") or block.get("content")
230
+ if isinstance(text, str):
231
+ parts.append(text)
232
+ return "".join(parts)
233
+ return str(content) if content is not None else ""
234
+
235
+
236
+ def _parse_scores(raw: str, valid_names: set[str]) -> list[tuple[str, int, str]]:
237
+ """Parse a voter's JSON scorecard, keeping only entries for *valid_names*.
238
+
239
+ Tolerant of code fences / surrounding prose: extracts the first JSON object.
240
+ Scores are clamped to 1–10. Self-votes and unknown names are dropped.
241
+ """
242
+ match = re.search(r"\{.*\}", raw, re.DOTALL)
243
+ if not match:
244
+ return []
245
+ try:
246
+ data = json.loads(match.group(0))
247
+ except (json.JSONDecodeError, ValueError):
248
+ return []
249
+
250
+ out: list[tuple[str, int, str]] = []
251
+ seen: set[str] = set()
252
+ for entry in data.get("scores", []) if isinstance(data, dict) else []:
253
+ if not isinstance(entry, dict):
254
+ continue
255
+ name = str(entry.get("agent", "")).strip()
256
+ if name not in valid_names or name in seen:
257
+ continue
258
+ try:
259
+ score = round(float(entry.get("score", 0)))
260
+ except (TypeError, ValueError):
261
+ continue
262
+ score = max(1, min(10, score))
263
+ reason = str(entry.get("reason", "")).strip()
264
+ out.append((name, score, reason))
265
+ seen.add(name)
266
+ return out
267
+
268
+
269
+ def _parse_vote(raw: str, valid_names: set[str]) -> tuple[str | None, str]:
270
+ """Parse a single-choice ballot: ``{"choice": name, "reason": ...}``.
271
+
272
+ Returns ``(choice_name, reason)`` where ``choice_name`` is None if the model
273
+ didn't pick a valid (non-self) advisor. Tolerant of code fences / prose.
274
+ """
275
+ match = re.search(r"\{.*\}", raw, re.DOTALL)
276
+ if not match:
277
+ return None, ""
278
+ try:
279
+ data = json.loads(match.group(0))
280
+ except (json.JSONDecodeError, ValueError):
281
+ return None, ""
282
+ if not isinstance(data, dict):
283
+ return None, ""
284
+ choice = str(data.get("choice", "")).strip()
285
+ reason = str(data.get("reason", "")).strip()
286
+ if choice not in valid_names:
287
+ return None, reason
288
+ return choice, reason
289
+
290
+
291
+ def get_council_model() -> Any:
292
+ """Build a chat model for the council from the session's configured provider.
293
+
294
+ Reuses :class:`~novacode_cli.config.model_manager.ModelManager` so the
295
+ council speaks with whatever model the user picked via ``/model`` (Ollama by
296
+ default). Raises if no provider can be created.
297
+ """
298
+ from novacode_cli.config.model_manager import MODEL_PRESETS, ModelManager
299
+
300
+ mm = ModelManager()
301
+ current = mm.get_current_provider() # (display_name, model_name) | None
302
+ provider_id = "ollama"
303
+ model_name: str | None = None
304
+ if current:
305
+ display, model_name = current
306
+ for pid, preset in MODEL_PRESETS.items():
307
+ if preset["name"] == display:
308
+ provider_id = pid
309
+ break
310
+ return mm.create_model_for_provider(provider_id, model_name) # type: ignore[arg-type]
311
+
312
+
313
+ async def _stream_persona_turn(
314
+ persona: Persona, convo: str, model: Any
315
+ ) -> AsyncIterator[tuple[str, str]]:
316
+ """Stream one persona's turn, allowing bounded web-search tool calls.
317
+
318
+ Yields ``("delta", text)`` for streamed prose and ``("tool", query)`` when
319
+ the persona runs a web search. Models without tool support (or that can't
320
+ stream tool calls) fall back to plain streaming.
321
+ """
322
+ system_text = persona.system + _RESPONSE_GUIDE
323
+
324
+ model_tools = None
325
+ if hasattr(model, "bind_tools"):
326
+ try:
327
+ model_tools = model.bind_tools([_SEARCH_TOOL_SCHEMA])
328
+ except Exception: # noqa: BLE001 - provider may not support tools
329
+ model_tools = None
330
+
331
+ if model_tools is None:
332
+ async for chunk in model.astream([("system", system_text), ("human", convo)]):
333
+ piece = _content_text(chunk)
334
+ if piece:
335
+ yield ("delta", piece)
336
+ return
337
+
338
+ # Dict-form messages so we don't import langchain_core.messages here; the
339
+ # model converts them. The model's own tool-call message (``gathered``) is
340
+ # appended as returned.
341
+ messages: list[Any] = [
342
+ {"role": "system", "content": system_text},
343
+ {"role": "user", "content": convo},
344
+ ]
345
+ rounds = 0
346
+ while True:
347
+ use_tools = rounds < _MAX_TOOL_ROUNDS
348
+ gathered = None
349
+ async for chunk in (model_tools if use_tools else model).astream(messages):
350
+ if use_tools:
351
+ try:
352
+ gathered = chunk if gathered is None else gathered + chunk
353
+ except (
354
+ Exception
355
+ ):
356
+ gathered = None
357
+ piece = _content_text(chunk)
358
+ if piece:
359
+ yield ("delta", piece)
360
+
361
+ tool_calls = (
362
+ list(getattr(gathered, "tool_calls", None) or [])
363
+ if gathered is not None
364
+ else []
365
+ )
366
+ if not tool_calls:
367
+ return
368
+
369
+ messages.append(gathered)
370
+ for call in tool_calls:
371
+ args = call.get("args", {}) or {}
372
+ query = str(args.get("query", "")).strip()
373
+ yield ("tool", query)
374
+ result = await asyncio.to_thread(_run_council_tool, call.get("name", ""), args)
375
+ messages.append(
376
+ {"role": "tool", "content": result, "tool_call_id": call.get("id", "")}
377
+ )
378
+ rounds += 1
379
+
380
+
381
+ async def _cast_ballot(
382
+ voter: Persona,
383
+ answered: list[Persona],
384
+ answers: dict[str, str],
385
+ topic: str,
386
+ model: Any,
387
+ ) -> tuple[Persona, str | None, str]:
388
+ """One ballot. Never raises — a failed vote is simply an abstention."""
389
+ others = [p for p in answered if p.id != voter.id]
390
+ if not others:
391
+ return voter, None, ""
392
+ valid_names = {p.name for p in others}
393
+
394
+ ballot = f"The question was:\n\n{topic}\n\nThe answers:\n\n"
395
+ for p in others:
396
+ ballot += f"=== {p.name} ===\n{answers.get(p.id, '')}\n\n"
397
+ ballot += "Vote for the single best answer."
398
+
399
+ try:
400
+ resp = await model.ainvoke(
401
+ [("system", voter.system + _VOTE_GUIDE), ("human", ballot)]
402
+ )
403
+ return voter, *_parse_vote(_content_text(resp), valid_names)
404
+ except asyncio.CancelledError:
405
+ raise
406
+ except Exception: # noqa: BLE001 — a bad ballot must not kill the run
407
+ logger.debug("council vote by %s failed", voter.id, exc_info=True)
408
+ return voter, None, ""
409
+
410
+ # Ballots are independent, so run them concurrently: voting was 5 sequential
411
+ # model calls purely because it was written as a loop. Results are emitted in
412
+ # council order so the UI stays deterministic.
413
+
414
+
415
+ async def run_council(
416
+ topic: str,
417
+ model: Any,
418
+ personas: list[Persona] | None = None,
419
+ history: list[dict[str, Any]] | None = None,
420
+ *,
421
+ member_models: dict[str, Any] | None = None,
422
+ advice_only: bool = False,
423
+ ) -> AsyncIterator[dict[str, Any]]:
424
+ """Run a council debate over *topic* and yield event dicts.
425
+
426
+ *history* carries prior rounds of the same session so the advisors can build
427
+ on an earlier discussion (follow-up questions).
428
+
429
+ Flow: members answer concurrently and **independently** (no peer context),
430
+ then each casts a **single democratic vote** for the best answer (never its
431
+ own); the **majority** answer is the verdict.
432
+
433
+ Event ``type``s, in order:
434
+ ``council_start`` -> for each persona ``agent_start`` / ``agent_delta``* /
435
+ ``agent_done`` -> ``vote_start`` -> for each persona ``vote`` ->
436
+ ``verdict`` -> ``done``.
437
+ """
438
+ members = personas or PERSONAS
439
+ models = dict(member_models or {})
440
+ name_to_id = {p.name: p.id for p in members}
441
+ id_to_name = {p.id: p.name for p in members}
442
+ history_block = _format_history(history)
443
+
444
+ yield {
445
+ "type": "council_start",
446
+ "topic": topic,
447
+ "agents": [
448
+ {"id": p.id, "name": p.name, "avatar": p.avatar, "color": p.color}
449
+ for p in members
450
+ ],
451
+ }
452
+
453
+ # Independent streams share only the output queue, never peer answers.
454
+ answers: dict[str, str] = {}
455
+
456
+ async def respond(persona: Persona) -> AsyncIterator[dict[str, Any]]:
457
+ yield {
458
+ "type": "agent_start",
459
+ "id": persona.id,
460
+ "name": persona.name,
461
+ "avatar": persona.avatar,
462
+ "color": persona.color,
463
+ }
464
+
465
+ convo = ""
466
+ if history_block:
467
+ convo += history_block + "\n\n"
468
+ convo += f"The question before the council:\n\n{topic}\n\n"
469
+ convo += "Give your own independent answer."
470
+
471
+ # One member failing must not end the council. A provider hiccup used to
472
+ # propagate out of run_council and kill the run mid-flight — the user
473
+ # lost the answers already on screen and got nothing. That member is
474
+ # dropped from the round instead; the rest still answer and vote.
475
+ full = ""
476
+ failed: str | None = None
477
+ try:
478
+ async for event in _stream_persona_turn(persona, convo, models.get(persona.id, model)):
479
+ if event[0] == "delta":
480
+ full += event[1]
481
+ yield {"type": "agent_delta", "id": persona.id, "text": event[1]}
482
+ else: # ("tool", query)
483
+ yield {"type": "agent_tool", "id": persona.id, "query": event[1]}
484
+ except asyncio.CancelledError:
485
+ raise # client disconnected / run cancelled — do not swallow
486
+ except Exception as exc: # noqa: BLE001 — one member must not end the run
487
+ failed = f"{type(exc).__name__}: {exc}"
488
+ logger.debug("council member %s failed", persona.id, exc_info=True)
489
+
490
+ text = full.strip()
491
+ if failed and not text:
492
+ yield {
493
+ "type": "agent_failed",
494
+ "id": persona.id,
495
+ "name": persona.name,
496
+ "message": failed,
497
+ }
498
+ return # Failed members are excluded from voting.
499
+
500
+ answers[persona.id] = text
501
+ yield {"type": "agent_done", "id": persona.id, "text": text}
502
+
503
+ events: asyncio.Queue = asyncio.Queue(maxsize=256)
504
+
505
+ async def collect(persona: Persona) -> None:
506
+ try:
507
+ async with asyncio.timeout(180):
508
+ async for event in respond(persona):
509
+ await events.put(event)
510
+ except Exception as exc:
511
+ answers.pop(persona.id, None)
512
+ await events.put({
513
+ "type": "agent_failed", "id": persona.id,
514
+ "name": persona.name, "message": str(exc),
515
+ })
516
+ finally:
517
+ if not asyncio.current_task().cancelling():
518
+ await events.put(None)
519
+
520
+ tasks = [asyncio.create_task(collect(persona)) for persona in members]
521
+ remaining = len(tasks)
522
+ try:
523
+ while remaining:
524
+ event = await events.get()
525
+ if event is None:
526
+ remaining -= 1
527
+ else:
528
+ yield event
529
+ finally:
530
+ for task in tasks:
531
+ task.cancel()
532
+ await asyncio.gather(*tasks, return_exceptions=True)
533
+
534
+ if advice_only:
535
+ yield {"type": "done"}
536
+ return
537
+
538
+ # --- Phase 2: democratic voting (one vote each) ----------------------
539
+ # Only members who actually produced an answer are on the ballot or hold a
540
+ # vote — a member that failed above must not be votable (nobody read an
541
+ # answer from it) and cannot cast one.
542
+ answered = [p for p in members if answers.get(p.id)]
543
+ if not answered:
544
+ yield {
545
+ "type": "council_error",
546
+ "message": "No advisor produced an answer — the model may be unavailable.",
547
+ }
548
+ yield {"type": "done"}
549
+ return
550
+
551
+ yield {"type": "vote_start"}
552
+ tally: dict[str, int] = {p.id: 0 for p in answered}
553
+
554
+ ballots = await asyncio.gather(
555
+ *(_cast_ballot(v, answered, answers, topic, models.get(v.id, model)) for v in answered)
556
+ )
557
+
558
+ for voter, choice_name, reason in ballots:
559
+ choice_id = name_to_id.get(choice_name) if choice_name else None
560
+ if choice_id is not None and choice_id in tally:
561
+ tally[choice_id] += 1
562
+
563
+ yield {
564
+ "type": "vote",
565
+ "voter": voter.id,
566
+ "voter_name": voter.name,
567
+ "choice": choice_id,
568
+ "choice_name": choice_name or "",
569
+ "reason": reason,
570
+ }
571
+
572
+ # --- Phase 3: majority verdict ---------------------------------------
573
+ # Winner = most votes, over the members who actually answered (a failed
574
+ # member is not in `tally`). Ties break by council order, so the outcome is
575
+ # deterministic rather than dict-order dependent. With no votes at all
576
+ # (every ballot abstained) the first answering member stands.
577
+ winner_id = (
578
+ max(answered, key=lambda p: (tally[p.id], -answered.index(p))).id
579
+ if any(tally.values())
580
+ else answered[0].id
581
+ )
582
+ top = tally[winner_id]
583
+ tied = [p.id for p in answered if tally[p.id] == top and top > 0]
584
+ yield {
585
+ "type": "verdict",
586
+ "winner_id": winner_id,
587
+ "winner_name": id_to_name.get(winner_id, "") if winner_id else "",
588
+ "tally": tally,
589
+ "votes": sum(tally.values()),
590
+ # Surfaced so the UI can say "tied, resolved by council order" rather
591
+ # than presenting a coin-flip as a clear majority.
592
+ "tied": len(tied) > 1,
593
+ "answer": answers.get(winner_id, "") if winner_id else "",
594
+ }
595
+ yield {"type": "done"}