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,565 @@
1
+ """Durable host decisions, usage reservations and independent worker attempts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import time
8
+ import uuid
9
+ from copy import deepcopy
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ if TYPE_CHECKING:
14
+ from novacode_cli.states.Session import SessionState
15
+
16
+ from novacode_cli.prime_factory import DEFAULT_OUTPUT_LIMIT, ROLES, VERSION
17
+ from novacode_cli.prime_factory.state import bounded_text, check_definition, digest, plan_definition
18
+ from novacode_cli.prime_factory.store import FactoryError, FactoryStore
19
+ from novacode_cli.prime_memory import project_context, safe_text
20
+
21
+ ACTIVE = {"running", "starting", "uncertain"}
22
+ MAX_ACTIVE_SECONDS = 86400
23
+ MAX_SPECIALISTS = 3
24
+
25
+
26
+ def storage_path(root: Path) -> Path:
27
+ """Share state across worktrees without writing into a tracked checkout."""
28
+ context = project_context(str(root))
29
+ return Path(context.get("common_dir") or root / ".nova") / "prime_factory" / "runs.sqlite3"
30
+
31
+
32
+ class FactoryController:
33
+ """Only host code can approve stages, certify checks or authorize publication."""
34
+
35
+ def __init__(
36
+ self, store: FactoryStore, record: dict[str, Any], session: SessionState | None = None
37
+ ) -> None:
38
+ """Bind one optimistic durable owner and its originating session."""
39
+ self.store, self.record, self.session = store, record, session
40
+ self.lock = asyncio.Lock()
41
+ self.handles: dict[str, Any] = {}
42
+ self.events: asyncio.Queue[Any] = asyncio.Queue()
43
+ self._clock = time.monotonic()
44
+ self._active = record["status"] == "running"
45
+ self._storage_failed = False
46
+ self._waiting: set[str] = set()
47
+ self._approval_epoch = 0
48
+ self._pending_approvals: set[asyncio.Future[Any]] = set()
49
+
50
+ @classmethod
51
+ def create(
52
+ cls,
53
+ root: Path,
54
+ goal: str,
55
+ thread: str,
56
+ routes: dict[str, Any],
57
+ *,
58
+ session: SessionState | None = None,
59
+ store: FactoryStore | None = None,
60
+ ) -> FactoryController:
61
+ """Persist intent before the first model call or workspace mutation."""
62
+ record = {
63
+ "version": VERSION,
64
+ "factory": True,
65
+ "id": uuid.uuid4().hex,
66
+ "thread": thread,
67
+ "root": str(root.resolve()),
68
+ "goal": bounded_text(goal),
69
+ "stage": "requirements",
70
+ "phase": "requirements",
71
+ "status": "running",
72
+ "criteria": [],
73
+ "tasks": [],
74
+ "sources": [],
75
+ "checks": [],
76
+ "attempts": [],
77
+ "approvals": {},
78
+ "blockers": [],
79
+ "routes": routes,
80
+ "output_token_limit": DEFAULT_OUTPUT_LIMIT,
81
+ "reservations": {},
82
+ "budget": {"tokens": 500_000, "active_seconds": 3600.0},
83
+ "usage": {"tokens": 0, "active_seconds": 0.0, "by_role": {}},
84
+ "candidate": None,
85
+ "workspace": None,
86
+ "isolation": "worktree",
87
+ "baseline_selection": None,
88
+ "publication": None,
89
+ }
90
+ controller = cls(store or FactoryStore(storage_path(root)), record, session)
91
+ controller.save()
92
+ return controller
93
+
94
+ def tick(self) -> None:
95
+ """Count active elapsed time once across all workers, never per worker."""
96
+ with self.store.lock:
97
+ now = time.monotonic()
98
+ workers = {a["id"] for a in self.record["attempts"] if a["status"] in ACTIVE}
99
+ all_waiting = bool(self._waiting) and workers <= self._waiting
100
+ if self._active and not all_waiting:
101
+ self.record["usage"]["active_seconds"] += max(0, now - self._clock)
102
+ self._clock = now
103
+
104
+ def begin_wait(self, key: str) -> None:
105
+ """Exclude human wait time only while every active worker is waiting."""
106
+ with self.store.lock:
107
+ self.tick()
108
+ self._waiting.add(key)
109
+
110
+ def end_wait(self, key: str) -> None:
111
+ """Finish a host-owned wait without charging the preceding idle interval."""
112
+ with self.store.lock:
113
+ self.tick()
114
+ self._waiting.discard(key)
115
+
116
+ def save(self) -> None:
117
+ """Fail closed on storage errors; no caller may dispatch after a failed save."""
118
+ with self.store.lock:
119
+ if self._storage_failed:
120
+ msg = "Factory storage failed; further mutations are blocked"
121
+ raise FactoryError(msg)
122
+ self.tick()
123
+ try:
124
+ self.store.save(self.record)
125
+ except Exception as exc:
126
+ self._storage_failed = True
127
+ if self.record["status"] not in {"cancelled", "published"}:
128
+ self.record["status"] = "paused"
129
+ self._active = False
130
+ msg = "Factory storage failed; further mutations are blocked"
131
+ raise FactoryError(msg) from exc
132
+ if (
133
+ self.session is not None
134
+ and getattr(self.session, "thread_id", self.record["thread"])
135
+ == self.record["thread"]
136
+ ):
137
+ self.session.prime_report = self.progress()
138
+
139
+ def progress(self) -> dict[str, Any]:
140
+ """Return a detached dashboard snapshot without raw worker transcripts."""
141
+ snapshot = {
142
+ key: deepcopy(value)
143
+ for key, value in self.record.items()
144
+ if key not in {"candidate", "attempts", "checks", "sources", "publication"}
145
+ }
146
+ candidate = self.record.get("candidate")
147
+ snapshot["candidate"] = (
148
+ {
149
+ key: candidate[key]
150
+ for key in ("id", "revision", "branch", "workspace")
151
+ if key in candidate
152
+ }
153
+ if candidate
154
+ else None
155
+ )
156
+ snapshot["attempts"] = [
157
+ {
158
+ key: deepcopy(value)
159
+ for key, value in attempt.items()
160
+ if key not in {"result", "output", "recovery"}
161
+ }
162
+ for attempt in self.record["attempts"][-64:]
163
+ ]
164
+ snapshot["checks"] = [
165
+ {
166
+ **{key: deepcopy(value) for key, value in check.items() if key != "evidence"},
167
+ "evidence_count": len(check["evidence"]),
168
+ "evidence": [
169
+ {key: value for key, value in evidence.items() if key != "output"}
170
+ for evidence in check["evidence"][-1:]
171
+ ],
172
+ }
173
+ for check in self.record["checks"]
174
+ ]
175
+ snapshot["sources"] = [
176
+ {
177
+ "id": source["id"],
178
+ "content": {
179
+ key: value
180
+ for key, value in source["content"].items()
181
+ if key not in {"excerpt", "decisions"}
182
+ },
183
+ }
184
+ for source in self.record["sources"]
185
+ ]
186
+ publication = self.record.get("publication")
187
+ snapshot["publication"] = (
188
+ {
189
+ key: publication[key]
190
+ for key in ("id", "status", "url", "destination", "branch", "base")
191
+ if key in publication
192
+ }
193
+ if publication
194
+ else None
195
+ )
196
+ return snapshot
197
+
198
+ def _invalidate_approvals(self) -> None:
199
+ self._approval_epoch += 1
200
+ for future in tuple(self._pending_approvals):
201
+
202
+ def reject(target: asyncio.Future[Any] = future) -> None:
203
+ if not target.done():
204
+ target.set_result({"response": {"approved": False}})
205
+
206
+ if not future.get_loop().is_closed():
207
+ future.get_loop().call_soon_threadsafe(reject)
208
+
209
+ def request_pause(self) -> None:
210
+ """Fence new work immediately; the driver stops owned workers at its next boundary."""
211
+ with self.store.lock:
212
+ self.tick()
213
+ self._active = False
214
+ self._invalidate_approvals()
215
+ if self.record["status"] not in {"cancelled", "published"}:
216
+ self.record["status"] = "paused"
217
+ if not self._storage_failed:
218
+ self.save()
219
+
220
+ def request_cancel(self) -> None:
221
+ """Preserve unfinished branches and prevent subsequent resume dispatch."""
222
+ with self.store.lock:
223
+ if self.record["status"] == "published":
224
+ msg = "Published runs cannot be cancelled"
225
+ raise FactoryError(msg)
226
+ self.tick()
227
+ self._active = False
228
+ self._invalidate_approvals()
229
+ self.record["status"] = "cancelled"
230
+ if not self._storage_failed:
231
+ self.save()
232
+
233
+ def set_budget(self, tokens: int, active_seconds: float) -> None:
234
+ """A human budget change invalidates plan and delivery approval."""
235
+ if type(tokens) is not int or tokens <= 0 or not 0 < active_seconds <= MAX_ACTIVE_SECONDS:
236
+ msg = "Budget requires positive tokens and at most 1440 active minutes"
237
+ raise FactoryError(msg)
238
+ self.record["budget"] = {"tokens": tokens, "active_seconds": active_seconds}
239
+ self.record["approvals"].clear()
240
+ self.request_pause()
241
+
242
+ def ensure_active(self) -> None:
243
+ """Check shared limits immediately before every dispatch and mutation."""
244
+ if (
245
+ self.session is not None
246
+ and getattr(self.session, "thread_id", self.record["thread"]) != self.record["thread"]
247
+ ):
248
+ self.request_pause()
249
+ message = "Conversation changed; the factory is paused until explicitly resumed"
250
+ raise FactoryError(message)
251
+ self.tick()
252
+ if self._storage_failed or self.record["status"] != "running":
253
+ msg = "Factory is suspended; explicit resume is required"
254
+ raise FactoryError(msg)
255
+ usage, budget = self.record["usage"], self.record["budget"]
256
+ reserved = sum(r["tokens"] for r in self.record["reservations"].values())
257
+ if usage["tokens"] + reserved >= budget["tokens"] or (
258
+ usage["active_seconds"] >= budget["active_seconds"]
259
+ ):
260
+ self.record["blockers"] = ["Shared factory budget exhausted"]
261
+ self.request_pause()
262
+ msg = "Shared factory budget exhausted"
263
+ raise FactoryError(msg)
264
+
265
+ async def reserve(self, attempt: str, call: str, tokens: int) -> dict[str, Any]:
266
+ """Reserve conservative capacity before allowing any provider request."""
267
+ async with self.lock:
268
+ self.ensure_attempt(attempt)
269
+ self.ensure_active()
270
+ if type(tokens) is not int or tokens <= 0 or not call:
271
+ msg = "Invalid model reservation"
272
+ raise FactoryError(msg)
273
+ key = attempt + ":" + call
274
+ if key in self.record["reservations"]:
275
+ msg = "Duplicate model reservation"
276
+ raise FactoryError(msg)
277
+ reserved = sum(r["tokens"] for r in self.record["reservations"].values())
278
+ if self.record["usage"]["tokens"] + reserved + tokens > self.record["budget"]["tokens"]:
279
+ self.record["blockers"] = ["Insufficient capacity for the next model call"]
280
+ self.request_pause()
281
+ raise FactoryError(self.record["blockers"][0])
282
+ self.record["reservations"][key] = {"tokens": tokens, "attempt": attempt}
283
+ self.save()
284
+ return {"reservation": key}
285
+
286
+ async def reconcile(self, attempt: str, key: str, actual: int | None) -> None:
287
+ """Charge unknown/failed calls conservatively; never silently reclaim uncertain usage."""
288
+ async with self.lock:
289
+ worker = self.ensure_attempt(attempt)
290
+ reservation = self.record["reservations"].get(key)
291
+ if not reservation or reservation["attempt"] != attempt:
292
+ msg = "Foreign or already reconciled model reservation"
293
+ raise FactoryError(msg)
294
+ if actual is not None and (type(actual) is not int or actual < 0):
295
+ msg = "Invalid provider usage"
296
+ raise FactoryError(msg)
297
+ amount = reservation["tokens"] if actual is None else actual
298
+ usage = self.record["usage"]
299
+ usage["tokens"] += amount
300
+ role = usage["by_role"].setdefault(worker["role"], {"tokens": 0, "estimated_calls": 0})
301
+ role["tokens"] += amount
302
+ role["estimated_calls"] += actual is None
303
+ del self.record["reservations"][key]
304
+ self.save()
305
+ if usage["tokens"] >= self.record["budget"]["tokens"]:
306
+ self.record["blockers"] = ["Shared factory budget exhausted"]
307
+ self.request_pause()
308
+
309
+ def ensure_attempt(self, identity: str) -> dict[str, Any]:
310
+ """Accept events only through the host-bound current attempt identity."""
311
+ found = next((a for a in self.record["attempts"] if a["id"] == identity), None)
312
+ if found is None or found["status"] not in ACTIVE:
313
+ msg = "Unknown, stale or completed worker attempt"
314
+ raise FactoryError(msg)
315
+ return found
316
+
317
+ def start_attempt(self, role: str, task: str, cwd: Path) -> dict[str, Any]:
318
+ """Persist trusted identity before allocating a process."""
319
+ self.ensure_active()
320
+ if (
321
+ role not in ROLES
322
+ or sum(a["status"] in ACTIVE for a in self.record["attempts"]) >= MAX_SPECIALISTS
323
+ ):
324
+ msg = "Factory specialist limit reached or invalid role"
325
+ raise FactoryError(msg)
326
+ if role in {"developer", "test_engineer"} and self.record["stage"] == "implementation":
327
+ self.require_plan()
328
+ elif role == "developer":
329
+ msg = "Implementation cannot begin before plan approval"
330
+ raise FactoryError(msg)
331
+ attempt = {
332
+ "id": uuid.uuid4().hex,
333
+ "run": self.record["id"],
334
+ "role": role,
335
+ "task": task,
336
+ "cwd": str(cwd.resolve()),
337
+ "status": "starting",
338
+ "route": self.record["routes"][role],
339
+ "recovery": None,
340
+ }
341
+ import psutil
342
+
343
+ isolation = self.record["isolation"]
344
+ container = isinstance(isolation, dict) and not task.startswith("command-")
345
+ attempt["recovery"] = {
346
+ "kind": "factory_start",
347
+ "attempt": attempt["id"],
348
+ "since": time.time(),
349
+ "owner": psutil.Process().username(),
350
+ "container": container,
351
+ "name": "nova-team-" + attempt["id"],
352
+ "labels": {
353
+ "nova.agent": attempt["id"],
354
+ "nova.team": self.record["id"],
355
+ "nova.root": self.record["id"],
356
+ "nova.project": self.record["root"],
357
+ "nova.attempt": attempt["id"],
358
+ },
359
+ }
360
+ self.record["attempts"].append(attempt)
361
+ self.save()
362
+ return attempt
363
+
364
+ def require_plan(self) -> None:
365
+ """Freeze scope, environment, models, budgets and acceptance after human review."""
366
+ approval = self.record["approvals"].get("plan")
367
+ if approval != digest(plan_definition(self.record)):
368
+ msg = "The current plan requires human approval"
369
+ raise FactoryError(msg)
370
+
371
+ def waive(self, target: str, reason: str) -> None:
372
+ """Record an explicit human exception for one check or finding on one candidate."""
373
+ candidate = self.record.get("candidate") or {}
374
+ known = {check["id"] for check in self.record["checks"]} | {
375
+ digest(finding) for finding in self.record.get("review_findings", [])
376
+ }
377
+ if (
378
+ self.record["status"] in {"cancelled", "published"}
379
+ or not candidate.get("id")
380
+ or target not in known
381
+ ):
382
+ message = "Waivers require a known check or finding on the current unfinished candidate"
383
+ raise FactoryError(message)
384
+ note = bounded_text(reason, 2000)
385
+ self.request_pause()
386
+ self.record.setdefault("waivers", {})[target] = {
387
+ "candidate": candidate["id"],
388
+ "reason": note,
389
+ }
390
+ self.save()
391
+
392
+ def waived(self, target: str, candidate: str) -> bool:
393
+ """A candidate change expires human exceptions without weakening acceptance definitions."""
394
+ return (self.record.get("waivers", {}).get(target) or {}).get("candidate") == candidate
395
+
396
+ def approve(self, kind: str, identity: str) -> None:
397
+ """Called only by the owning UI response handler, never exposed as a model tool."""
398
+ if self.record["status"] not in {"running", "waiting_approval"}:
399
+ msg = "Suspended or terminal runs cannot accept an expired approval"
400
+ raise FactoryError(msg)
401
+ expected = (
402
+ digest(plan_definition(self.record))
403
+ if kind == "plan"
404
+ else (self.record.get("publication") or {}).get("id")
405
+ )
406
+ if kind not in {"plan", "delivery"} or not expected or expected != identity:
407
+ msg = "Approval does not match the current plan or publication intent"
408
+ raise FactoryError(msg)
409
+ self.record["approvals"][kind] = identity
410
+ self.save()
411
+
412
+ async def stop_workers(self) -> None:
413
+ """Keep uncertain workers fenced until termination is proven."""
414
+ for key, handle in list(self.handles.items()):
415
+ try:
416
+ stopped = await handle.stop()
417
+ except Exception: # noqa: BLE001 - One uncertain worker cannot prevent cleanup of peers.
418
+ stopped = False
419
+ attempt = next((a for a in self.record["attempts"] if a["id"] == key), None)
420
+ if attempt is not None:
421
+ attempt["status"] = "stopped" if stopped else "uncertain"
422
+ if stopped:
423
+ self.handles.pop(key, None)
424
+ if not self._storage_failed:
425
+ self.save()
426
+
427
+ async def resume(self) -> None:
428
+ """Reconcile old workers and reservations before explicitly restarting dispatch."""
429
+ if self.record["status"] in {"cancelled", "published"}:
430
+ msg = "Cancelled or published runs cannot be resumed"
431
+ raise FactoryError(msg)
432
+ from novacode_cli.teams.recovery import RecoveryHandle
433
+
434
+ for attempt in self.record["attempts"]:
435
+ if attempt["status"] in ACTIVE:
436
+ recovery = attempt.get("recovery") or {}
437
+ if recovery.get("kind") == "factory_start":
438
+ from novacode_cli.prime_factory.recovery import reconcile_start
439
+
440
+ if not await reconcile_start(recovery):
441
+ msg = "Worker startup remains uncertain; no work was replayed"
442
+ raise FactoryError(msg)
443
+ attempt["status"] = "stopped"
444
+ continue
445
+ handle = self.handles.get(attempt["id"]) or RecoveryHandle(
446
+ attempt.get("recovery") or {}
447
+ )
448
+ if not await handle.stop():
449
+ msg = "An uncertain worker still owns the workspace"
450
+ raise FactoryError(msg)
451
+ attempt["status"] = "stopped"
452
+ for key, reservation in list(self.record["reservations"].items()):
453
+ self.record["usage"]["tokens"] += reservation["tokens"]
454
+ attempt = next(
455
+ (a for a in self.record["attempts"] if a["id"] == reservation["attempt"]), None
456
+ )
457
+ role_name = attempt["role"] if attempt else "unknown"
458
+ role = self.record["usage"]["by_role"].setdefault(
459
+ role_name, {"tokens": 0, "estimated_calls": 0}
460
+ )
461
+ role["tokens"] += reservation["tokens"]
462
+ role["estimated_calls"] += 1
463
+ del self.record["reservations"][key]
464
+ self.record["approvals"].pop("delivery", None)
465
+ self.record["blockers"] = []
466
+ self.record["status"] = "running"
467
+ self._waiting.clear()
468
+ self._invalidate_approvals()
469
+ self._clock = time.monotonic()
470
+ self._active = True
471
+ self.save()
472
+ self.ensure_active()
473
+
474
+ async def wait_approval(self, kind: str, details: dict[str, Any], identity: str) -> bool:
475
+ """Yield an ordinary routed plan approval, mandatory even with tool auto approval."""
476
+ from novacode_cli import ui_events as ev
477
+
478
+ epoch = self._approval_epoch
479
+ wait_key = "approval:" + kind
480
+ self.begin_wait(wait_key)
481
+ self.record["status"] = "waiting_approval"
482
+ self.save()
483
+ future = asyncio.get_running_loop().create_future()
484
+ self._pending_approvals.add(future)
485
+ await self.events.put(
486
+ ev.InterruptRequest(
487
+ "plan",
488
+ {
489
+ "plan": json.dumps(details, ensure_ascii=False, indent=2),
490
+ "factory": True,
491
+ "requires_owner_approval": True,
492
+ "run_id": self.record["id"],
493
+ "continuation_id": self.record["id"],
494
+ "approval_kind": kind,
495
+ "approval_digest": identity,
496
+ },
497
+ future,
498
+ )
499
+ )
500
+ try:
501
+ result = await future
502
+ finally:
503
+ self._pending_approvals.discard(future)
504
+ self.end_wait(wait_key)
505
+ with self.store.lock:
506
+ if epoch != self._approval_epoch or self.record["status"] != "waiting_approval":
507
+ return False
508
+ response = result.get("response", {}) if isinstance(result, dict) else {}
509
+ approved = isinstance(response, dict) and response.get("approved") is True
510
+ if not approved:
511
+ self.record["blockers"] = [
512
+ f"Human {kind} approval required; resume {self.record['id']}"
513
+ ]
514
+ self.request_pause()
515
+ return False
516
+ self.approve(kind, identity)
517
+ self.record["status"] = "running"
518
+ self._clock = time.monotonic()
519
+ self._active = True
520
+ self.save()
521
+ return True
522
+
523
+ def check_result(
524
+ self,
525
+ check: dict[str, Any],
526
+ candidate: str,
527
+ environment: str,
528
+ argv: list[str],
529
+ code: int | None,
530
+ output: str,
531
+ *,
532
+ baseline: bool = False,
533
+ ) -> None:
534
+ """Only host-observed exact executions can satisfy frozen acceptance checks."""
535
+ self.require_plan()
536
+ if check not in self.record["checks"] or argv != check["argv"]:
537
+ msg = "Acceptance definition changed"
538
+ raise FactoryError(msg)
539
+ evidence = {
540
+ "id": uuid.uuid4().hex,
541
+ "check": check["id"],
542
+ "candidate": candidate,
543
+ "environment": environment,
544
+ "definition": digest(check_definition(check)),
545
+ "exit_code": code,
546
+ "output": safe_text(output, 12000),
547
+ "observed_at": time.time(),
548
+ "baseline": baseline,
549
+ }
550
+ check["evidence"].append(evidence)
551
+ if baseline:
552
+ check["baseline_failed"] = (
553
+ code is not None
554
+ and code not in {0, 126, 127, 9009}
555
+ and all(term in output for term in check.get("failure_contains", []))
556
+ )
557
+ else:
558
+ passed = code == 0 and all(term in output for term in check.get("contains", []))
559
+ unavailable = code is None or code in {126, 127, 9009}
560
+ check["status"] = "passed" if passed else "unavailable" if unavailable else "failed"
561
+ if not passed:
562
+ check["failures"] += 1
563
+ if check["test_first"] and not check.get("baseline_failed"):
564
+ check["status"] = "unverified"
565
+ self.save()