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,303 @@
1
+ """Which provider and model an async-agent graph runs on, chosen per dispatch.
2
+
3
+ The async graphs are built once, when the LangGraph server boots, so their model used
4
+ to be fixed for the life of the process: changing it meant editing ``.env`` and
5
+ recreating the container. This module makes it a per-run choice instead.
6
+
7
+ The CLI attaches the stored ``async`` role to every dispatch as the run's ``context``
8
+ (:func:`current_async_context`), and :class:`AsyncModelOverrideMiddleware` swaps that
9
+ model in for the run. A deployment driven purely by ``docker compose`` still behaves
10
+ exactly as before: no context on the wire means the environment wins
11
+ (:func:`novacode_cli.config.model_create.build_async_agent_model`).
12
+
13
+ Precedence, in one place: a stored ``async`` role beats the environment, because it is
14
+ the later and more specific statement of intent -- the user picked it in ``/model``.
15
+ The one exception is a per-graph variable such as ``PLAN_SCOUT_MODEL``, which is set
16
+ with a single agent in mind and therefore still wins over the shared role.
17
+
18
+ Models are built with :func:`novacode_cli.config.model_create.build_chat_model` rather
19
+ than langchain's ``init_chat_model``, because the latter does not know Nova's own
20
+ providers (``opencode``, ``nvidia``).
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import asyncio
26
+ import logging
27
+ import os
28
+ from dataclasses import dataclass
29
+ from typing import TYPE_CHECKING, Any
30
+
31
+ from langchain.agents.middleware.types import AgentMiddleware
32
+
33
+ from novacode_cli.agents.model_budget import (
34
+ _MODEL_CALL_BUDGET,
35
+ )
36
+ from novacode_cli.agents.model_budget import (
37
+ _MODEL_REQUEST_TIMEOUT as _MODEL_REQUEST_TIMEOUT,
38
+ )
39
+ from novacode_cli.agents.model_budget import (
40
+ _bounded_model as _bounded_model,
41
+ )
42
+
43
+ if TYPE_CHECKING:
44
+ from langchain_core.language_models import BaseChatModel
45
+
46
+ logger = logging.getLogger(__name__)
47
+
48
+ #: Models already built, keyed by ``(provider, model)``. A swap happens on every model
49
+ #: call, so building per call would rebuild a client dozens of times per run. A
50
+ #: concurrent miss builds twice and the last write wins: one extra object, no harm.
51
+ _MODEL_CACHE: dict[tuple[str, str], BaseChatModel] = {}
52
+
53
+ #: Whether :func:`install_async_model_context` has already wrapped the factories.
54
+ _PATCHED = False
55
+
56
+
57
+ @dataclass
58
+ class NovaAsyncContext:
59
+ """The run context the CLI attaches to every async dispatch.
60
+
61
+ Two plain fields, so the CLI and the graphs can share this without either importing
62
+ the other's machinery.
63
+ """
64
+
65
+ provider: str | None = None
66
+ model: str | None = None
67
+ #: The dispatching session's approval setting; delegates auto-approve independently.
68
+ auto_approve: bool = False
69
+ #: Host-provided owner intent; a subagent task description cannot supply consent.
70
+ user_request: str = ""
71
+
72
+
73
+ def _fields(context: Any) -> tuple[str, str]:
74
+ """Read ``(provider, model)`` off *context*, however it arrived.
75
+
76
+ A run context is rehydrated from JSON on the server, so it may be this dataclass or
77
+ a plain mapping depending on how the graph was invoked. Both are accepted.
78
+ """
79
+ if context is None:
80
+ return "", ""
81
+ if isinstance(context, dict):
82
+ provider, model_name = context.get("provider"), context.get("model")
83
+ else:
84
+ provider = getattr(context, "provider", None)
85
+ model_name = getattr(context, "model", None)
86
+ return str(provider or "").strip(), str(model_name or "").strip()
87
+
88
+
89
+ def build_context_model(context: Any) -> BaseChatModel | None:
90
+ """Return the model *context* asks for, or ``None`` to leave the graph alone.
91
+
92
+ A context that cannot be built (unknown provider, missing credentials) is logged
93
+ and ignored rather than raised: one bad choice must not fail a background run that
94
+ would otherwise have worked on the graph's own model.
95
+ """
96
+ provider, model_name = _fields(context)
97
+ if not provider or not model_name:
98
+ return None
99
+
100
+ key = (provider, model_name)
101
+ cached = _MODEL_CACHE.get(key)
102
+ if cached is not None:
103
+ return cached
104
+
105
+ from novacode_cli.config.model_create import build_chat_model
106
+
107
+ try:
108
+ built = build_chat_model(provider, model_name)
109
+ except Exception: # noqa: BLE001 - any failure means "use the graph default"
110
+ logger.warning(
111
+ "Async run asked for %s:%s but it could not be built; using the graph default",
112
+ provider,
113
+ model_name,
114
+ exc_info=True,
115
+ )
116
+ return None
117
+
118
+ _MODEL_CACHE[key] = built
119
+ return built
120
+
121
+
122
+ class AsyncModelOverrideMiddleware(AgentMiddleware):
123
+ """Run the graph on the run's model, for that run only.
124
+
125
+ Outermost on purpose: it has to see every model call before any provider-specific
126
+ middleware handles it.
127
+ """
128
+
129
+ def __init__(self, *, per_agent_model_var: str | None = None) -> None:
130
+ """Bind the middleware to one graph.
131
+
132
+ Args:
133
+ per_agent_model_var: An environment variable that pins this graph's model,
134
+ such as plan-scout's ``PLAN_SCOUT_MODEL``. When it is set the run
135
+ context is ignored, so a setting made for this one agent still wins over
136
+ the shared ``async`` role.
137
+ """
138
+ super().__init__()
139
+ self._per_agent_model_var = per_agent_model_var
140
+
141
+ def _model_for(self, request: Any) -> BaseChatModel | None:
142
+ """The model this request should use, or ``None`` to leave it as it is."""
143
+ var = self._per_agent_model_var
144
+ if var and os.environ.get(var, "").strip():
145
+ return None
146
+ return build_context_model(getattr(request.runtime, "context", None))
147
+
148
+ def wrap_model_call(self, request: Any, handler: Any) -> Any: # noqa: ANN401
149
+ """Route the call to the run's model when it named one."""
150
+ model = self._model_for(request)
151
+ return handler(request if model is None else request.override(model=model))
152
+
153
+ async def awrap_model_call(self, request: Any, handler: Any) -> Any: # noqa: ANN401
154
+ """Async twin of :meth:`wrap_model_call`.
155
+
156
+ The first build of a model reads Nova's settings: the working directory,
157
+ and each provider key through a lock file (``time.sleep``, ``os.unlink``).
158
+ On the server's event loop those are blocking calls, which ``langgraph
159
+ dev`` refuses outright, so the build failed and every run silently fell
160
+ back to the graph's default model. A cached model is returned inline;
161
+ only a miss goes to a thread.
162
+ """
163
+ key = _fields(getattr(request.runtime, "context", None))
164
+ if not all(key) or key in _MODEL_CACHE: # nothing to build, or already built
165
+ model = self._model_for(request)
166
+ else:
167
+ model = await asyncio.to_thread(self._model_for, request)
168
+ from novacode_cli.agents.model_retry import NovaSubagentRetryMiddleware
169
+
170
+ routed = request if model is None else request.override(model=model)
171
+ return await NovaSubagentRetryMiddleware(call_budget=_MODEL_CALL_BUDGET).awrap_model_call(
172
+ routed, handler
173
+ )
174
+
175
+
176
+ def current_async_context() -> NovaAsyncContext | None:
177
+ """The model async agents should run on, as a run context.
178
+
179
+ In order: the stored ``async`` role; else, when ``ASYNC_AGENT_PROVIDER`` or
180
+ ``ASYNC_AGENT_MODEL`` is set, ``None`` (the environment is an explicit
181
+ instruction, and the graphs read it themselves); else the main agent's own
182
+ model. That last step is what makes the agents follow whatever provider the
183
+ session runs on: they used to fall back to a fixed Ollama model, a leftover
184
+ of the container they ran in, which failed for anyone without Ollama.
185
+ """
186
+ from novacode_cli.config.nova_config import NovaConfig
187
+
188
+ role = NovaConfig().get_role_model("async") or {}
189
+ provider = str(role.get("provider") or "").strip()
190
+ model_name = str(role.get("model") or "").strip()
191
+ if provider and model_name:
192
+ return NovaAsyncContext(provider=provider, model=model_name)
193
+
194
+ if (
195
+ os.environ.get("ASYNC_AGENT_PROVIDER", "").strip()
196
+ or os.environ.get("ASYNC_AGENT_MODEL", "").strip()
197
+ ):
198
+ return None
199
+ try:
200
+ from novacode_cli.utils.model_info import get_model_info
201
+
202
+ provider, model_name, _display = get_model_info()
203
+ except Exception: # noqa: BLE001 — no main model to inherit: leave it to the graphs
204
+ return None
205
+ if not provider or not model_name:
206
+ return None
207
+ return NovaAsyncContext(provider=str(provider), model=str(model_name))
208
+
209
+
210
+ def install_async_model_context() -> None:
211
+ """Make every async dispatch carry the stored ``async`` role as run context.
212
+
213
+ deepagents builds the dispatch itself -- ``runs.create(thread_id, assistant_id,
214
+ input)`` -- and passes no ``context``, so the remote graph has no way to know which
215
+ model the user picked. Wrapping the client factory is the narrowest seam for that:
216
+ the request is otherwise left exactly as deepagents made it.
217
+
218
+ The context is read lazily, at dispatch time, so a ``/model`` change applies to the
219
+ very next launch rather than the next server restart. ``runs.create`` already
220
+ accepts ``context`` in langgraph-sdk and the graphs declare ``context_schema``, so
221
+ this is only the wiring in between.
222
+
223
+ Idempotent: calling it twice leaves the factories wrapped once.
224
+ """
225
+ global _PATCHED # noqa: PLW0603 - the guard is the point
226
+ if _PATCHED:
227
+ return
228
+ _PATCHED = True
229
+
230
+ from deepagents.middleware import async_subagents
231
+
232
+ for name in ("get_client", "get_sync_client"):
233
+ factory = getattr(async_subagents, name, None)
234
+ if factory is None or getattr(factory, "_nova_context_bound", False):
235
+ continue
236
+
237
+ def wrapped(*args: Any, _factory: Any = factory, **kwargs: Any) -> Any:
238
+ """Bind the run context onto the client the factory just built."""
239
+ from novacode_cli.agents.async_task_errors import bind_run_errors
240
+
241
+ return bind_run_errors(_bind_context(_factory(*args, **kwargs)))
242
+
243
+ wrapped._nova_context_bound = True # type: ignore[attr-defined]
244
+ setattr(async_subagents, name, wrapped)
245
+
246
+
247
+ def _bind_context(client: Any) -> Any:
248
+ """Return *client* with ``runs.create`` defaulting ``context`` to the async role."""
249
+ # `getattr` with a default widens to `Any | None`, so a client that exposes no
250
+ # `runs` is left alone and the type is narrowed before the monkey-patch below.
251
+ runs: Any = getattr(client, "runs", None)
252
+ if runs is None:
253
+ return client
254
+
255
+ original = getattr(runs, "create", None)
256
+ if original is None or getattr(original, "_nova_context_bound", False):
257
+ return client
258
+
259
+ def create(*args: Any, **kwargs: Any) -> Any:
260
+ """Create the run, naming the async role's model unless one was given."""
261
+ if kwargs.get("context") is None:
262
+ from dataclasses import replace
263
+
264
+ from novacode_cli.tools.plan_mode_tools import _auto_approve_var
265
+
266
+ context = current_async_context()
267
+ from novacode_cli.security.approval_context import current_user_request
268
+
269
+ owner_request = current_user_request.get()
270
+ if owner_request is not None:
271
+ context = replace(context or NovaAsyncContext(), user_request=owner_request)
272
+ # Fixed at launch: the run cannot see a later toggle in this session.
273
+ if _auto_approve_var.get():
274
+ context = replace(context or NovaAsyncContext(), auto_approve=True)
275
+ if context is not None:
276
+ config = kwargs.get("config")
277
+ configurable = config.get("configurable") if isinstance(config, dict) else None
278
+ # deepagents uses this configurable only as a fallback for finding
279
+ # the parent. Its async launcher also puts parent_thread_id in the
280
+ # child thread metadata, which is the supported lookup path when a
281
+ # run context is present. LangGraph rejects a run containing both.
282
+ if isinstance(configurable, dict) and "deepagents_parent" in configurable:
283
+ configurable = dict(configurable)
284
+ configurable.pop("deepagents_parent", None)
285
+ config = dict(config)
286
+ if configurable:
287
+ config["configurable"] = configurable
288
+ else:
289
+ config.pop("configurable", None)
290
+ if config:
291
+ kwargs["config"] = config
292
+ else:
293
+ kwargs.pop("config", None)
294
+ # Preserve unrelated configurable values. This run can still launch
295
+ # with its graph default, while avoiding the server's 400 response.
296
+ remaining = config.get("configurable") if isinstance(config, dict) else None
297
+ if not remaining:
298
+ kwargs["context"] = context
299
+ return original(*args, **kwargs)
300
+
301
+ create._nova_context_bound = True # type: ignore[attr-defined]
302
+ runs.create = create
303
+ return client
@@ -0,0 +1,52 @@
1
+ """Start Nova's own agent server when an async agent is first dispatched.
2
+
3
+ The server used to be started while Nova booted, whether or not the session ever
4
+ delegated anything: a second Python process holding six compiled graphs, on a
5
+ machine that may have no memory to spare. It is now only *planned* at boot (a
6
+ port is reserved, so the specs can name it) and started here, by the first
7
+ ``start_async_task``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ from typing import Any
14
+
15
+ from langchain.agents.middleware.types import AgentMiddleware
16
+ from langchain_core.messages import ToolMessage
17
+
18
+ #: The dispatch tool. The others (check/update/cancel/list) only make sense once
19
+ #: something has been started, so they never trigger a launch.
20
+ START_TOOL = "start_async_task"
21
+
22
+ _FAILED = (
23
+ "The async agent server could not be started, so no background task was "
24
+ "launched. Delegate this with the `task` tool (an in-process subagent) instead."
25
+ )
26
+
27
+
28
+ class AsyncServerOnDemandMiddleware(AgentMiddleware):
29
+ """Bring the planned agent server up before the first async dispatch."""
30
+
31
+ async def awrap_tool_call(self, request: Any, handler: Any) -> Any: # noqa: ANN401
32
+ call = getattr(request, "tool_call", None) or {}
33
+ if call.get("name") == START_TOOL:
34
+ from novacode_cli.agents import server_launcher
35
+
36
+ if not await server_launcher.ensure_started():
37
+ return ToolMessage(content=_FAILED, tool_call_id=call.get("id", ""), status="error")
38
+ return await handler(request)
39
+
40
+ def wrap_tool_call(self, request: Any, handler: Any) -> Any: # noqa: ANN401
41
+ """Sync twin, for a subagent invoked with ``invoke``.
42
+
43
+ That runs on a worker thread with no event loop of its own, so the
44
+ launch gets a short-lived one.
45
+ """
46
+ call = getattr(request, "tool_call", None) or {}
47
+ if call.get("name") == START_TOOL:
48
+ from novacode_cli.agents import server_launcher
49
+
50
+ if not asyncio.run(server_launcher.ensure_started()):
51
+ return ToolMessage(content=_FAILED, tool_call_id=call.get("id", ""), status="error")
52
+ return handler(request)
@@ -0,0 +1,75 @@
1
+ # ruff: noqa: INP001 - agents uses namespace packaging
2
+ """Recover failed async-run diagnostics from LangGraph's checkpoint tasks."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import inspect
7
+ import logging
8
+ from typing import Any
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def _with_error(run: dict[str, Any], state: dict[str, Any], run_id: str) -> dict[str, Any]:
14
+ # A resumed thread may already have a newer checkpoint from a different run.
15
+ if (state.get("metadata") or {}).get("run_id") != run_id:
16
+ return run
17
+ from novacode_cli.security.prompt_injection import _credential_values, _redact
18
+ from novacode_cli.session.history_export import redact
19
+
20
+ secrets = _credential_values()
21
+ errors = []
22
+ for task in state.get("tasks") or []:
23
+ error = task.get("error")
24
+ if error:
25
+ errors.append(redact(_redact(str(error), secrets)))
26
+ if not errors:
27
+ return run
28
+ return {**run, "error": "\n".join(errors)[:4000]}
29
+
30
+
31
+ def bind_run_errors(client: Any) -> Any: # noqa: ANN401
32
+ """Enrich runs.get only when the server omitted a failed run's error.
33
+
34
+ LangGraph's Run schema has no error field; errors live in ThreadState.tasks.
35
+ Keep deepagents' normal check tool and state updates by filling that gap at
36
+ its existing client-factory seam. Never fetch or return child transcripts.
37
+ """
38
+ runs: Any = getattr(client, "runs", None)
39
+ original = getattr(runs, "get", None)
40
+ if original is None or getattr(original, "_nova_errors_bound", False) is True:
41
+ return client
42
+
43
+ def needs_error(run: dict[str, Any]) -> bool:
44
+ return run.get("status") == "error" and not run.get("error")
45
+
46
+ if inspect.iscoroutinefunction(original):
47
+
48
+ async def aget(thread_id: str, run_id: str, **kwargs: Any) -> Any: # noqa: ANN401
49
+ run = await original(thread_id, run_id, **kwargs)
50
+ if needs_error(run):
51
+ try:
52
+ state = await client.threads.get_state(thread_id=thread_id)
53
+ return _with_error(run, state, run_id)
54
+ except Exception: # noqa: BLE001 - status remains usable if state is unavailable
55
+ logger.debug("Failed to fetch async error checkpoint for run %s", run_id)
56
+ return run
57
+
58
+ replacement: Any = aget
59
+ else:
60
+
61
+ def get(thread_id: str, run_id: str, **kwargs: Any) -> Any: # noqa: ANN401
62
+ run = original(thread_id, run_id, **kwargs)
63
+ if needs_error(run):
64
+ try:
65
+ state = client.threads.get_state(thread_id=thread_id)
66
+ return _with_error(run, state, run_id)
67
+ except Exception: # noqa: BLE001 - status remains usable if state is unavailable
68
+ logger.debug("Failed to fetch async error checkpoint for run %s", run_id)
69
+ return run
70
+
71
+ replacement = get
72
+
73
+ replacement._nova_errors_bound = True
74
+ runs.get = replacement
75
+ return client
@@ -0,0 +1,27 @@
1
+ """The directory the async-agent graphs work in.
2
+
3
+ The graphs used ``Path.cwd()`` of the server process, which is wherever
4
+ ``langgraph.json`` lives: Nova's own repo. Launched from another project they
5
+ scouted Nova's source and reported it as findings about that project.
6
+
7
+ The launcher now names the session's workspace in ``NOVA_WORKSPACE_ROOT`` and the
8
+ graphs read it here. Without the variable (the Docker container, or a server
9
+ someone started by hand) the server's own directory is used, as before.
10
+
11
+ Resolved once, at import: ``os.getcwd()`` is a blocking call that ``langgraph
12
+ dev`` rejects on its event loop.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import os
18
+ from pathlib import Path
19
+
20
+ WORKSPACE_ROOT_VAR = "NOVA_WORKSPACE_ROOT"
21
+
22
+ _ROOT = Path(os.environ.get(WORKSPACE_ROOT_VAR, "").strip() or os.getcwd()).resolve()
23
+
24
+
25
+ def workspace_root() -> Path:
26
+ """Where the graphs read, search and run commands."""
27
+ return _ROOT
@@ -0,0 +1,165 @@
1
+ """Configurable commit and PR attribution without repository hooks or history edits."""
2
+ # ruff: noqa: INP001 -- agents is an existing namespace package.
3
+
4
+ from __future__ import annotations
5
+
6
+ import asyncio
7
+ import re
8
+ from typing import TYPE_CHECKING
9
+
10
+ from langchain.agents.middleware.types import AgentMiddleware
11
+ from langchain_core.messages import SystemMessage
12
+
13
+ from novacode_cli.config.nova_config import (
14
+ DEFAULT_ATTRIBUTION_EMAIL,
15
+ DEFAULT_ATTRIBUTION_NAME,
16
+ NovaConfig,
17
+ )
18
+
19
+ if TYPE_CHECKING:
20
+ from collections.abc import Awaitable, Callable
21
+
22
+ from langchain.agents.middleware.types import ModelRequest, ModelResponse, ToolCallRequest
23
+ from langchain_core.messages import AIMessage, ToolMessage
24
+ from langgraph.types import Command
25
+
26
+ COAUTHOR = f"Co-Authored-By: {DEFAULT_ATTRIBUTION_NAME} <{DEFAULT_ATTRIBUTION_EMAIL}>"
27
+ LOGO_URL = "https://raw.githubusercontent.com/Babitdor/NovaCode/main/assets/nova-attribution.png"
28
+ PR_FOOTER = (
29
+ f'<img src="{LOGO_URL}" alt="NovaCode logo" width="24" height="24" /> '
30
+ "Generated with [NovaCode](https://github.com/Babitdor/NovaCode)"
31
+ )
32
+ _TRAILER = re.compile(r"^[A-Za-z0-9][A-Za-z0-9-]*\s*:")
33
+
34
+
35
+ def commit_message(message: str, *, coauthor: str = COAUTHOR) -> str:
36
+ """Append a co-author once, preserving the final existing trailer block."""
37
+ stripped = message.rstrip()
38
+ lines = stripped.splitlines()
39
+ start = max((i + 1 for i, line in enumerate(lines) if not line.strip()), default=0)
40
+ tail = lines[start:]
41
+ trailers = bool(
42
+ start
43
+ and tail
44
+ and _TRAILER.match(tail[0])
45
+ and all(_TRAILER.match(line) or line.startswith((" ", "\t")) for line in tail)
46
+ )
47
+ if trailers and any(line.strip().casefold() == coauthor.casefold() for line in tail):
48
+ return message
49
+ separator = "\n" if trailers else "\n\n"
50
+ return stripped + separator + coauthor + "\n"
51
+
52
+
53
+ def pull_request_body(body: str) -> str:
54
+ """Append a compact repository-hosted logo footer once."""
55
+ if PR_FOOTER in body:
56
+ return body
57
+ return body.rstrip() + "\n\n---\n\n" + PR_FOOTER
58
+
59
+
60
+ def attribution_instructions(preferences: dict[str, bool], *, coauthor: str = COAUTHOR) -> str:
61
+ """Describe the effective preference for shell commands and other transports."""
62
+ commits = (
63
+ "For Git commits you create, append this trailer once, in the final trailer block:\n"
64
+ f"{coauthor}\n"
65
+ "Use git commit --trailer or a commit-message file so quoting is correct "
66
+ "for the current shell."
67
+ if preferences["commits"]
68
+ else "Do not add automatic NovaCode co-author trailers to commits."
69
+ )
70
+ prs = (
71
+ "For pull requests you create, append this exact logo footer once after the description:\n"
72
+ f"{PR_FOOTER}\n"
73
+ "For gh pr create, prefer --body-file with UTF-8 Markdown."
74
+ if preferences["pullRequests"]
75
+ else "Do not add automatic NovaCode text, logos or generated-by footers to pull requests."
76
+ )
77
+ return (
78
+ "<nova_attribution>\n" + commits + "\n" + prs + "\n"
79
+ "These preferences do not authorize commits, pushes or PR creation. Preserve the human "
80
+ "author, existing co-authors and user-supplied content. Do not install Git hooks, change "
81
+ "global Git identity, amend history or create extra commits for attribution. "
82
+ "A co-author trailer credits a commit; it does not establish line-level provenance.\n"
83
+ "</nova_attribution>"
84
+ )
85
+
86
+
87
+ class AttributionMiddleware(AgentMiddleware):
88
+ """Apply live preferences to requests and structured creation tool payloads."""
89
+
90
+ @staticmethod
91
+ def prepare(request: ModelRequest) -> ModelRequest:
92
+ """Append preferences without editing stored history or the base prompt."""
93
+ config = NovaConfig()
94
+ identity = config.get_attribution_identity()
95
+ coauthor = f"Co-Authored-By: {identity['name']} <{identity['email']}>"
96
+ note = attribution_instructions(config.get_attribution(), coauthor=coauthor)
97
+ original = request.system_message
98
+ content = original.content if original else ""
99
+ content = (
100
+ content + "\n\n" + note
101
+ if isinstance(content, str)
102
+ else [*content, {"type": "text", "text": note}]
103
+ )
104
+ system = (
105
+ original.model_copy(update={"content": content})
106
+ if original
107
+ else SystemMessage(content=content)
108
+ )
109
+ return request.override(system_message=system)
110
+
111
+ @staticmethod
112
+ def prepare_tool(request: ToolCallRequest) -> ToolCallRequest:
113
+ """Normalize supported structured payloads; never rewrite shell command strings."""
114
+ name = request.tool_call["name"].lower()
115
+ actions = ("create_pull_request", "git_commit", "create_or_update_file", "push_files")
116
+ action = next((a for a in actions if name == a or name.endswith("_" + a)), None)
117
+ if action is None:
118
+ return request
119
+ config = NovaConfig()
120
+ preferences = config.get_attribution()
121
+ args = dict(request.tool_call.get("args") or {})
122
+ if action == "create_pull_request" and preferences["pullRequests"]:
123
+ if "body" not in args:
124
+ schema = request.tool.args if request.tool else {}
125
+ if "body" not in schema:
126
+ return request
127
+ body = args.get("body", "")
128
+ if not isinstance(body, str):
129
+ return request # Leave malformed arguments for normal schema validation.
130
+ args["body"] = pull_request_body(body)
131
+ elif action != "create_pull_request" and preferences["commits"]:
132
+ if not isinstance(args.get("message"), str):
133
+ return request
134
+ identity = config.get_attribution_identity()
135
+ coauthor = f"Co-Authored-By: {identity['name']} <{identity['email']}>"
136
+ args["message"] = commit_message(args["message"], coauthor=coauthor)
137
+ else:
138
+ return request
139
+ return request.override(tool_call={**request.tool_call, "args": args})
140
+
141
+ def wrap_model_call(
142
+ self, request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse]
143
+ ) -> ModelResponse | AIMessage:
144
+ """Supply attribution preferences to synchronous model calls."""
145
+ return handler(self.prepare(request))
146
+
147
+ async def awrap_model_call(
148
+ self, request: ModelRequest, handler: Callable[[ModelRequest], Awaitable[ModelResponse]]
149
+ ) -> ModelResponse | AIMessage:
150
+ """Supply preferences without blocking the event loop on config reads."""
151
+ return await handler(await asyncio.to_thread(self.prepare, request))
152
+
153
+ def wrap_tool_call(
154
+ self, request: ToolCallRequest, handler: Callable[[ToolCallRequest], ToolMessage | Command]
155
+ ) -> ToolMessage | Command:
156
+ """Normalize supported synchronous tool calls before execution."""
157
+ return handler(self.prepare_tool(request))
158
+
159
+ async def awrap_tool_call(
160
+ self,
161
+ request: ToolCallRequest,
162
+ handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]],
163
+ ) -> ToolMessage | Command:
164
+ """Normalize supported async tool calls without blocking config reads."""
165
+ return await handler(await asyncio.to_thread(self.prepare_tool, request))