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,261 @@
1
+ """Canonical event-loop entry and explicit suspended-run continuation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import re
7
+ import uuid
8
+ from pathlib import Path
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ if TYPE_CHECKING:
12
+ from collections.abc import AsyncIterator
13
+
14
+ from novacode_cli import ui_events as ev
15
+ from novacode_cli.prime_factory import DEFAULT_OUTPUT_LIMIT, ROLES
16
+ from novacode_cli.prime_factory.controller import FactoryController, storage_path
17
+ from novacode_cli.prime_factory.runner import SpecialistRunner
18
+ from novacode_cli.prime_factory.store import FactoryError, FactoryStore
19
+ from novacode_cli.prime_factory.workflow import FactoryWorkflow
20
+ from novacode_cli.prime_memory import session_directory
21
+
22
+
23
+ def is_factory_request(text: str, session: Any) -> bool: # noqa: ANN401 - Session proxies.
24
+ """Only explicit development goals or selected continuations start factory work."""
25
+ if getattr(session, "prime_enabled", False) is not True:
26
+ return False
27
+ normalized = text.strip().lower()
28
+ if re.match(
29
+ r"^(?:(?:can you|could you|please)\s+)?"
30
+ r"(?:update me\b|test results\b|what\b|why\b|how\b|explain\b|"
31
+ r"describe\b|show\b|summarize\b|status\b)",
32
+ normalized,
33
+ ):
34
+ return False
35
+ if getattr(session, "prime_factory_resume", None):
36
+ return True
37
+ if normalized.startswith("continue") and getattr(session, "prime_pending", None):
38
+ return True
39
+ return bool(
40
+ re.match(
41
+ r"^(?:(?:please|can you|could you|i want (?:you )?to)\s+)?"
42
+ r"(?:fix|build|implement|add|create|replace|refactor|remove|update|upgrade|develop|"
43
+ r"resolve|repair|complete|test|optimize|migrate|finish)\b",
44
+ normalized,
45
+ )
46
+ )
47
+
48
+
49
+ def model_routes(profile: Any, session: Any, agent: Any) -> dict[str, Any]: # noqa: ANN401 - Framework routes.
50
+ """Inherit the actual bound session model while keeping configured roles exact."""
51
+ inherited = getattr(agent, "factory_model_route", None) or profile.get_model_config() or {}
52
+ override = getattr(session, "cli_model_override", None)
53
+ if isinstance(override, str) and ":" in override:
54
+ provider, model = override.split(":", 1)
55
+ inherited = {"provider": provider, "model": model}
56
+ routes = {role: profile.get_factory_model(role) or inherited for role in ROLES}
57
+ session_provider = getattr(session, "session_model_provider", None)
58
+ if any(not route.get("provider") or not route.get("model") for route in routes.values()):
59
+ message = "Factory requires explicit provider/model routes or a serializable session model"
60
+ raise FactoryError(message)
61
+ if (
62
+ session_provider
63
+ and session_provider != inherited.get("provider")
64
+ and any(not profile.get_factory_model(role) for role in ROLES)
65
+ ):
66
+ message = "Session model route is unavailable; configure factory role models"
67
+ raise FactoryError(message)
68
+ return routes
69
+
70
+
71
+ async def prepare_controller(
72
+ text: str,
73
+ session: Any, # noqa: ANN401 - Session proxies.
74
+ agent: Any, # noqa: ANN401 - Compiled graph implementations.
75
+ store: FactoryStore,
76
+ root: Path,
77
+ *,
78
+ requested_thread: str | None = None,
79
+ ) -> FactoryController:
80
+ """Load a durable continuation or persist a new explicitly requested goal."""
81
+ requested_thread = session.thread_id if requested_thread is None else requested_thread
82
+ if session.thread_id != requested_thread:
83
+ message = "Conversation changed during factory startup; no work was dispatched"
84
+ raise FactoryError(message)
85
+ identity = getattr(session, "prime_factory_resume", None)
86
+ if identity:
87
+ record = await asyncio.to_thread(store.load, identity)
88
+ saved_root = await asyncio.to_thread(Path(record["root"]).resolve)
89
+ if saved_root != root:
90
+ message = "Resume from the factory's original repository workspace"
91
+ raise FactoryError(message)
92
+ record.setdefault("origin_thread", record["thread"])
93
+ record["thread"] = requested_thread
94
+ controller = FactoryController(store, record, session)
95
+ await controller.resume()
96
+ controller.ensure_active()
97
+ session.prime_factory_resume = None
98
+ if record["stage"] not in {"requirements", "research", "architecture", "plan_approval"}:
99
+ record["stage"] = record["phase"] = "plan_approval"
100
+ controller.save()
101
+ session.prime_factory = controller
102
+ return controller
103
+ existing = getattr(session, "prime_report", None)
104
+ if (
105
+ isinstance(existing, dict)
106
+ and existing.get("factory")
107
+ and existing.get("status")
108
+ not in {
109
+ "published",
110
+ "cancelled",
111
+ }
112
+ ):
113
+ message = f"Resume or cancel existing factory run {existing['id']} first"
114
+ raise FactoryError(message)
115
+ from novacode_cli.config.nova_config import NovaConfig
116
+
117
+ profile = await asyncio.to_thread(NovaConfig)
118
+ routes = await asyncio.to_thread(model_routes, profile, session, agent)
119
+ output_limit = profile.get("max_tokens") or DEFAULT_OUTPUT_LIMIT
120
+ if type(output_limit) is not int or output_limit <= 0:
121
+ message = "Factory model output limit must be a positive integer"
122
+ raise FactoryError(message)
123
+ pending = getattr(session, "prime_pending", None)
124
+ goal = pending.get("goal", text) if isinstance(pending, dict) else text
125
+ controller = await asyncio.to_thread(
126
+ FactoryController.create,
127
+ root,
128
+ goal,
129
+ requested_thread,
130
+ routes,
131
+ session=session,
132
+ store=store,
133
+ )
134
+ container = profile.get("team_container")
135
+ controller.record["output_token_limit"] = output_limit
136
+ controller.save()
137
+ if container is not None:
138
+ if not isinstance(container, dict):
139
+ message = "Configured container profile is invalid"
140
+ raise FactoryError(message)
141
+ controller.record["isolation"] = {
142
+ "workers": "container",
143
+ "integration_checks": "worktree",
144
+ "container_profile": {
145
+ key: container.get(key)
146
+ for key in (
147
+ "image",
148
+ "network",
149
+ "user",
150
+ "memory",
151
+ "cpus",
152
+ "readonly",
153
+ )
154
+ },
155
+ }
156
+ controller.save()
157
+ controller.ensure_active()
158
+ session.prime_pending = None
159
+ session.prime_factory = controller
160
+ return controller
161
+
162
+
163
+ async def iterate_factory_events( # noqa: PLR0912, PLR0915 - Event stream and cancellation cleanup.
164
+ text: str,
165
+ session: Any, # noqa: ANN401 - Session proxies.
166
+ *,
167
+ agent: Any = None, # noqa: ANN401 - Compiled graph implementations.
168
+ ) -> AsyncIterator[Any]:
169
+ """Drive host transitions while keeping approval routing in the originating consumer."""
170
+ controller = None
171
+ driver = None
172
+ store = None
173
+ originating_thread = session.thread_id
174
+ try:
175
+ root = await asyncio.to_thread(lambda: Path(session_directory(session)).resolve())
176
+ store = await asyncio.to_thread(FactoryStore, storage_path(root))
177
+ controller = await prepare_controller(
178
+ text,
179
+ session,
180
+ agent,
181
+ store,
182
+ root,
183
+ requested_thread=originating_thread,
184
+ )
185
+ if agent is not None:
186
+ from langchain_core.messages import HumanMessage
187
+
188
+ await agent.aupdate_state(
189
+ {"configurable": {"thread_id": controller.record["thread"]}},
190
+ {"messages": [HumanMessage(text, id=uuid.uuid4().hex)]},
191
+ )
192
+ runner = SpecialistRunner(controller)
193
+ workflow = await asyncio.to_thread(FactoryWorkflow, controller, runner)
194
+ driver = asyncio.create_task(
195
+ workflow.run(), name=f"prime-factory-{controller.record['id']}"
196
+ )
197
+ while not driver.done() or not controller.events.empty():
198
+ try:
199
+ event = await asyncio.wait_for(controller.events.get(), timeout=0.25)
200
+ except TimeoutError:
201
+ continue
202
+ if session.thread_id != controller.record["thread"]:
203
+ controller.request_pause()
204
+ if isinstance(event, ev.InterruptRequest) and not event.future.done():
205
+ event.future.set_result({"response": {"approved": False}})
206
+ continue
207
+ yield event
208
+ await driver
209
+ if session.thread_id != controller.record["thread"]:
210
+ yield ev.Done(had_response=False)
211
+ return
212
+ report = controller.record
213
+ message = f"Prime factory {report['id']} · {report['status']} · {report['stage']}"
214
+ if report["blockers"]:
215
+ message += "\n" + "\n".join(report["blockers"])
216
+ if report["status"] not in {"published", "cancelled"}:
217
+ message += f"\nContinue with /prime resume {report['id']}."
218
+ if agent is not None:
219
+ from langchain_core.messages import AIMessage
220
+
221
+ await agent.aupdate_state(
222
+ {"configurable": {"thread_id": controller.record["thread"]}},
223
+ {"messages": [AIMessage(message, id=uuid.uuid4().hex)]},
224
+ )
225
+ if session.thread_id != controller.record["thread"]:
226
+ yield ev.Done(had_response=False)
227
+ return
228
+ yield ev.ContextMessage(
229
+ message,
230
+ event_type="nova_prime_receipt",
231
+ record_id=report["id"],
232
+ data=controller.progress(),
233
+ )
234
+ # Headless consumers need a useful continuation in their final result too.
235
+ yield ev.AssistantMessage(text=message, agent_name="Prime", agent_color="cyan")
236
+ yield ev.Done(had_response=True)
237
+ except asyncio.CancelledError:
238
+ if controller is not None:
239
+ controller.request_pause()
240
+ raise
241
+ except Exception as exc: # noqa: BLE001 - Frontends receive durable failure state.
242
+ if controller is not None and not controller._storage_failed:
243
+ controller.record["blockers"] = [str(exc)]
244
+ controller.request_pause()
245
+ if session.thread_id != originating_thread:
246
+ yield ev.Done(had_response=False)
247
+ return
248
+ yield ev.Error(f"Prime factory suspended: {exc}")
249
+ yield ev.Done(had_response=False)
250
+ finally:
251
+ if driver is not None and not driver.done():
252
+ driver.cancel()
253
+ await asyncio.gather(driver, return_exceptions=True)
254
+ if controller is not None:
255
+ try:
256
+ await controller.stop_workers()
257
+ finally:
258
+ if getattr(session, "prime_factory", None) is controller:
259
+ session.prime_factory = None
260
+ if store is not None:
261
+ await asyncio.to_thread(store.close)
@@ -0,0 +1,201 @@
1
+ """Validated factory plans and immutable acceptance definitions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from pathlib import PurePosixPath, PureWindowsPath
8
+ from typing import Any
9
+
10
+ from novacode_cli.prime_factory.store import FactoryError
11
+
12
+ MAX_OWNED_FILES = 128
13
+ MAX_TASKS = 64
14
+ MAX_CHECKS = 32
15
+ MAX_ARGV = 64
16
+ MAX_ASSERTIONS = 8
17
+
18
+
19
+ def digest(value: object) -> str:
20
+ """Hash lossless canonical data for approvals and observed evidence."""
21
+ return hashlib.sha256(json.dumps(value, sort_keys=True, allow_nan=False).encode()).hexdigest()
22
+
23
+
24
+ def bounded_text(value: object, limit: int = 4000) -> str:
25
+ """Reject truncated or missing worker assertions."""
26
+ if not isinstance(value, str) or not value.strip() or len(value) > limit:
27
+ msg = "Expected bounded nonempty text"
28
+ raise FactoryError(msg)
29
+ return value
30
+
31
+
32
+ def paths(value: object) -> list[str]:
33
+ """Validate repository-relative ownership, including Windows on Linux hosts."""
34
+ if not isinstance(value, list) or len(value) > MAX_OWNED_FILES:
35
+ msg = "Expected at most 128 owned files"
36
+ raise FactoryError(msg)
37
+ result = []
38
+ for item in value:
39
+ name = bounded_text(item, 500).replace("\\", "/")
40
+ path = PurePosixPath(name)
41
+ if (
42
+ path.is_absolute()
43
+ or PureWindowsPath(name).drive
44
+ or ".." in path.parts
45
+ or any(part in {".git", ".nova", ".env", ".aws", ".codex"} for part in path.parts)
46
+ or any(char in name for char in "*?[]\x00\n\r")
47
+ ):
48
+ msg = "Ownership must name explicit repository files"
49
+ raise FactoryError(msg)
50
+ result.append(path.as_posix())
51
+ if len(result) != len(set(result)):
52
+ msg = "Duplicate file ownership"
53
+ raise FactoryError(msg)
54
+ return result
55
+
56
+
57
+ def validate_tasks(value: object) -> list[dict[str, Any]]:
58
+ """Require an acyclic dependency graph before approving any implementation."""
59
+ if not isinstance(value, list) or not 1 <= len(value) <= MAX_TASKS:
60
+ msg = "Plan requires 1-64 tasks"
61
+ raise FactoryError(msg)
62
+ tasks: list[dict[str, Any]] = []
63
+ for raw in value:
64
+ if not isinstance(raw, dict):
65
+ msg = "Invalid task"
66
+ raise FactoryError(msg)
67
+ key = bounded_text(raw.get("id"), 60)
68
+ if not key.replace("_", "").replace("-", "").isalnum():
69
+ msg = "Task IDs must be simple identifiers"
70
+ raise FactoryError(msg)
71
+ dependencies = raw.get("depends_on", [])
72
+ if not isinstance(dependencies, list) or any(not isinstance(d, str) for d in dependencies):
73
+ msg = "Invalid task dependencies"
74
+ raise FactoryError(msg)
75
+ tasks.append(
76
+ {
77
+ "id": key,
78
+ "description": bounded_text(raw.get("description")),
79
+ "files": paths(raw.get("files")),
80
+ "depends_on": dependencies,
81
+ "status": "pending",
82
+ "attempts": [],
83
+ }
84
+ )
85
+ keys = {task["id"] for task in tasks}
86
+ if len(keys) != len(tasks):
87
+ msg = "Duplicate task IDs"
88
+ raise FactoryError(msg)
89
+ resolved: set[str] = set()
90
+ while len(resolved) < len(tasks):
91
+ ready = {t["id"] for t in tasks if set(t["depends_on"]) <= resolved}
92
+ if ready <= resolved:
93
+ msg = "Task dependencies contain a cycle or unknown task"
94
+ raise FactoryError(msg)
95
+ resolved |= ready
96
+ return tasks
97
+
98
+
99
+ def check_definition(check: dict[str, Any]) -> dict[str, Any]:
100
+ """Keep approval and execution signatures on the same acceptance fields."""
101
+ return {
102
+ **{key: check[key] for key in ("argv", "criteria", "purpose", "test_first", "security")},
103
+ **{key: check.get(key, []) for key in ("tasks", "contains", "failure_contains")},
104
+ }
105
+
106
+
107
+ def validate_checks(
108
+ value: object, criteria: list[str], task_ids: set[str] | list[str] | None = None
109
+ ) -> list[dict[str, Any]]:
110
+ """Assign identity from definitions so renaming cannot reset repair limits."""
111
+ if not isinstance(value, list) or not 1 <= len(value) <= MAX_CHECKS:
112
+ msg = "Define 1-32 acceptance checks"
113
+ raise FactoryError(msg)
114
+ result: list[dict[str, Any]] = []
115
+ for raw in value:
116
+ if not isinstance(raw, dict):
117
+ msg = "Invalid acceptance definition"
118
+ raise FactoryError(msg)
119
+ argv = raw.get("argv")
120
+ indexes = raw.get("criteria")
121
+ if not isinstance(argv, list) or not argv or len(argv) > MAX_ARGV:
122
+ msg = "Checks require command argument arrays"
123
+ raise FactoryError(msg)
124
+ argv = [bounded_text(arg, 2000) for arg in argv]
125
+ if (
126
+ not isinstance(indexes, list)
127
+ or not indexes
128
+ or any(type(i) is not int or not 0 <= i < len(criteria) for i in indexes)
129
+ ):
130
+ msg = "Checks must cover existing acceptance criteria"
131
+ raise FactoryError(msg)
132
+ owners = raw.get("tasks", sorted(task_ids or []))
133
+ if (
134
+ not isinstance(owners, list)
135
+ or any(not isinstance(owner, str) for owner in owners)
136
+ or len(owners) != len(set(owners))
137
+ or (task_ids is not None and (not owners or not set(owners) <= set(task_ids)))
138
+ ):
139
+ msg = "Acceptance tasks must identify responsible planned tasks"
140
+ raise FactoryError(msg)
141
+ assertions = {}
142
+ for field in ("contains", "failure_contains"):
143
+ expected = raw.get(field, [])
144
+ if not isinstance(expected, list) or len(expected) > MAX_ASSERTIONS:
145
+ msg = "Acceptance output assertions require at most eight literals"
146
+ raise FactoryError(msg)
147
+ assertions[field] = [bounded_text(term, 500) for term in expected]
148
+ definition = {
149
+ "argv": argv,
150
+ "criteria": sorted(set(indexes)),
151
+ "purpose": bounded_text(raw.get("purpose")),
152
+ "test_first": raw.get("test_first") is True,
153
+ "security": raw.get("security") is True,
154
+ "tasks": sorted(owners),
155
+ **assertions,
156
+ }
157
+ result.append(
158
+ {
159
+ **definition,
160
+ "id": digest(definition),
161
+ "failures": 0,
162
+ "status": "pending",
163
+ "evidence": [],
164
+ }
165
+ )
166
+ if len({c["id"] for c in result}) != len(result):
167
+ msg = "Duplicate acceptance definitions"
168
+ raise FactoryError(msg)
169
+ if {i for c in result for i in c["criteria"]} != set(range(len(criteria))):
170
+ msg = "Every criterion needs an acceptance check"
171
+ raise FactoryError(msg)
172
+ return result
173
+
174
+
175
+ def plan_definition(record: dict[str, Any]) -> dict[str, Any]:
176
+ """Include every approval-relevant field while excluding mutable task/check status."""
177
+ return {
178
+ "goal": record["goal"],
179
+ "criteria": record["criteria"],
180
+ "tasks": [
181
+ {k: t[k] for k in ("id", "description", "files", "depends_on")} for t in record["tasks"]
182
+ ],
183
+ "checks": [{"id": c["id"], **check_definition(c)} for c in record["checks"]],
184
+ **{
185
+ key: record.get(key)
186
+ for key in (
187
+ "decisions",
188
+ "sources",
189
+ "research_requirements",
190
+ "research_exemption",
191
+ "isolation",
192
+ "routes",
193
+ "budget",
194
+ "output_token_limit",
195
+ "setup",
196
+ "test_files",
197
+ "destination",
198
+ "baseline_selection",
199
+ )
200
+ },
201
+ }
@@ -0,0 +1,128 @@
1
+ """Atomic factory snapshots with optimistic writer fencing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sqlite3
7
+ from threading import RLock
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ from novacode_cli.prime_factory import DEFAULT_OUTPUT_LIMIT, VERSION
11
+
12
+ if TYPE_CHECKING:
13
+ from pathlib import Path
14
+
15
+
16
+ class FactoryError(ValueError):
17
+ """A factory invariant requires intervention before work can continue."""
18
+
19
+
20
+ def listing(record: dict[str, Any]) -> dict[str, Any]:
21
+ """Project bounded display metadata separately from execution and publication artifacts."""
22
+ result = {
23
+ key: record[key]
24
+ for key in (
25
+ "id",
26
+ "version",
27
+ "factory",
28
+ "thread",
29
+ "root",
30
+ "goal",
31
+ "stage",
32
+ "phase",
33
+ "status",
34
+ )
35
+ if key in record
36
+ }
37
+ result["summary_only"] = True
38
+ result["checks"] = [
39
+ {"id": check["id"], "status": check["status"], "purpose": check.get("purpose", "")[:300]}
40
+ for check in record.get("checks", [])
41
+ ]
42
+ return result
43
+
44
+
45
+ class FactoryStore:
46
+ """Commit complete snapshots before dispatching side effects."""
47
+
48
+ def __init__(self, path: Path) -> None:
49
+ """Open repository-local storage; failures propagate to the caller."""
50
+ path.parent.mkdir(parents=True, exist_ok=True)
51
+ self.lock = RLock()
52
+ self.db = sqlite3.connect(path, timeout=5, check_same_thread=False)
53
+ self.db.execute("PRAGMA journal_mode=WAL")
54
+ self.db.execute("PRAGMA synchronous=FULL")
55
+ self.db.execute(
56
+ "CREATE TABLE IF NOT EXISTS runs (id TEXT PRIMARY KEY, revision INTEGER, body TEXT)"
57
+ )
58
+ columns = {row[1] for row in self.db.execute("PRAGMA table_info(runs)")}
59
+ if "summary" not in columns:
60
+ self.db.execute("ALTER TABLE runs ADD COLUMN summary TEXT")
61
+ self.db.commit()
62
+
63
+ def save(self, record: dict[str, Any]) -> None:
64
+ """Reject stale controllers rather than allowing two schedulers to dispatch."""
65
+ with self.lock:
66
+ revision = record.get("store_revision", 0)
67
+ snapshot = {**record, "store_revision": revision + 1}
68
+ encoded = json.dumps(snapshot, allow_nan=False, sort_keys=True)
69
+ from novacode_cli.security.prompt_injection import _credential_values
70
+
71
+ if any(json.dumps(secret)[1:-1] in encoded for secret in _credential_values()):
72
+ message = "Factory records cannot contain credentials; use environment references"
73
+ raise FactoryError(message)
74
+ summary = json.dumps(listing(snapshot), allow_nan=False)
75
+ with self.db:
76
+ if revision == 0:
77
+ self.db.execute(
78
+ "INSERT INTO runs (id, revision, body, summary) VALUES (?, ?, ?, ?)",
79
+ (record["id"], 1, encoded, summary),
80
+ )
81
+ else:
82
+ cursor = self.db.execute(
83
+ "UPDATE runs SET revision=?, body=?, summary=? WHERE id=? AND revision=?",
84
+ (revision + 1, encoded, summary, record["id"], revision),
85
+ )
86
+ if cursor.rowcount != 1:
87
+ msg = "Factory ownership changed; reload before continuing"
88
+ raise FactoryError(msg)
89
+ record["store_revision"] = revision + 1
90
+
91
+ def load(self, identity: str) -> dict[str, Any]:
92
+ """Read only this repository's versioned factory records."""
93
+ with self.lock:
94
+ row = self.db.execute("SELECT body FROM runs WHERE id=?", (identity,)).fetchone()
95
+ if row is None:
96
+ msg = "Factory run not found in this repository"
97
+ raise FactoryError(msg)
98
+ record = json.loads(row[0])
99
+ if record.get("version") != VERSION or record.get("factory") is not True:
100
+ msg = "Unsupported factory record version"
101
+ raise FactoryError(msg)
102
+ record.setdefault("output_token_limit", DEFAULT_OUTPUT_LIMIT)
103
+ return record
104
+
105
+ def records(self) -> list[dict[str, Any]]:
106
+ """Return bounded recent summaries without loading command output."""
107
+ with self.lock:
108
+ rows = self.db.execute(
109
+ "SELECT id, summary FROM runs ORDER BY rowid DESC LIMIT 50"
110
+ ).fetchall()
111
+ result = []
112
+ for identity, stored_summary in rows:
113
+ summary = stored_summary
114
+ if summary is None:
115
+ # Upgrade one old record at a time, never materialize fifty full histories.
116
+ record = self.load(identity)
117
+ summary = json.dumps(listing(record))
118
+ with self.db:
119
+ self.db.execute(
120
+ "UPDATE runs SET summary=? WHERE id=? AND revision=?",
121
+ (summary, identity, record["store_revision"]),
122
+ )
123
+ result.append(json.loads(summary))
124
+ return result
125
+
126
+ def close(self) -> None:
127
+ """Release the database after all owned workers have stopped."""
128
+ self.db.close()