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,60 @@
1
+ """Lessons every installation starts with.
2
+
3
+ Running the learning loop across a dozen unrelated tasks, the only things that
4
+ earned a place in the shared pool were environment facts that are true for
5
+ everyone: run ``apt-get update`` first, how to get Python into a bare container.
6
+ There is no reason for each user to pay to rediscover those, so they ship here.
7
+
8
+ They are recalled exactly like learned lessons — only when what is on screen
9
+ matches — and are never injected wholesale. Each is written symptom-first,
10
+ because the symptom is what a session is looking at when it needs the lesson.
11
+
12
+ Every entry below was hit by an agent on Terminal-Bench and resolved the way it
13
+ says. Keep it that way: add something only once it has been seen to work.
14
+
15
+ This is a Python module rather than a data file because the package ships only
16
+ ``*.jinja`` as package data.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import os
22
+
23
+ TOPIC = "builtin-environment"
24
+
25
+ LESSONS: tuple[str, ...] = (
26
+ "- **Symptom:** `E: Unable to locate package <name>` from `apt-get install` in a fresh "
27
+ "container. **Cause:** the image ships with no package lists. **Fix:** run `apt-get update` "
28
+ "first, then install.",
29
+ "- **Symptom:** `apt-get install` stops at a `Geographic area:` / `Configuring tzdata` menu, or "
30
+ "hangs with no output. **Cause:** debconf is waiting for an answer and there is no terminal. "
31
+ "**Fix:** `DEBIAN_FRONTEND=noninteractive apt-get install -y <packages>`.",
32
+ "- **Symptom:** `bash: python3: command not found` or `python: command not found`. **Cause:** "
33
+ "the image has no Python (or only `python3`, not `python`). **Fix:** `apt-get update && "
34
+ "apt-get install -y python3 python3-pip`, and call it as `python3`.",
35
+ "- **Symptom:** `error: externally-managed-environment` from `pip install`. **Cause:** PEP 668 — "
36
+ "the system Python refuses global installs. **Fix:** `pip install --break-system-packages "
37
+ "<pkg>` in a throwaway container, or create a venv with `python3 -m venv`.",
38
+ "- **Symptom:** `E: Could not get lock /var/lib/dpkg/lock` or `lock-frontend`. **Cause:** "
39
+ "another apt/dpkg process is still running. **Fix:** wait for it to finish (`while pgrep -x "
40
+ "apt-get >/dev/null || pgrep -x dpkg >/dev/null; do sleep 2; done`); do not delete the lock.",
41
+ "- **Symptom:** `Could not find a suitable TLS CA certificate bundle` or SSL certificate "
42
+ "errors from pip/curl in a minimal image. **Cause:** no CA certificates installed. **Fix:** "
43
+ "`apt-get install -y ca-certificates`.",
44
+ "- **Symptom:** `curl: command not found` (or `timeout: failed to run command 'curl'`). "
45
+ "**Cause:** minimal image. **Fix:** `apt-get install -y curl`, or use `wget` or Python's "
46
+ "`urllib` if one of those is present.",
47
+ "- **Symptom:** a command takes minutes and its output comes back truncated. **Cause:** it "
48
+ "printed megabytes (a binary dump, a runaway loop, a huge log). **Fix:** bound the output — "
49
+ "`| head -c 2000`, `| tail -n 40`, or compare hashes/sizes instead of printing content.",
50
+ )
51
+
52
+
53
+ def enabled() -> bool:
54
+ """Off with ``NOVA_BUILTIN_LESSONS=0``."""
55
+ return os.environ.get("NOVA_BUILTIN_LESSONS", "").strip().lower() not in {"0", "false", "off"}
56
+
57
+
58
+ def body() -> str:
59
+ """The lessons as a topic-file body."""
60
+ return "# Environment basics\n\n" + "\n".join(LESSONS) + "\n"
@@ -0,0 +1,192 @@
1
+ """When Nova may dream: the three gates in front of automatic memory consolidation.
2
+
3
+ A dream (``/dream``) consolidates the memory wiki. Run automatically, it must
4
+ neither over-dream (burning tokens on a wiki that barely changed) nor
5
+ under-dream (letting a busy week's lessons sit unconsolidated). It runs only
6
+ when all three gates pass:
7
+
8
+ - **Time gate:** at least :data:`TIME_GATE` since the last dream.
9
+ - **Session gate:** at least :data:`SESSION_GATE` sessions (with messages)
10
+ started since the last dream.
11
+ - **Lock gate:** the consolidation lock is free, so two Nova processes (or a
12
+ manual ``/dream`` and an automatic one) never consolidate at once.
13
+
14
+ A manual ``/dream`` skips the time and session gates (the user asked) but still
15
+ takes the lock, and any finished dream resets both counters.
16
+
17
+ State lives beside the memory it guards: ``<agent_dir>/.dream.json`` and the
18
+ lock file ``<agent_dir>/.dream.lock``.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import contextlib
24
+ import json
25
+ import os
26
+ from dataclasses import dataclass
27
+ from datetime import UTC, datetime, timedelta
28
+ from typing import TYPE_CHECKING
29
+
30
+ if TYPE_CHECKING:
31
+ from pathlib import Path
32
+
33
+ #: Minimum time between dreams.
34
+ TIME_GATE = timedelta(hours=24)
35
+ #: Minimum sessions started since the last dream.
36
+ SESSION_GATE = 5
37
+ #: A lock older than this belongs to a dream that died; it may be taken over.
38
+ STALE_LOCK = timedelta(hours=2)
39
+
40
+ _STATE = ".dream.json"
41
+ _LOCK = ".dream.lock"
42
+
43
+
44
+ def _now() -> datetime:
45
+ return datetime.now(UTC)
46
+
47
+
48
+ def _read_state(agent_dir: Path) -> dict:
49
+ try:
50
+ return json.loads((agent_dir / _STATE).read_text(encoding="utf-8"))
51
+ except (OSError, ValueError):
52
+ return {}
53
+
54
+
55
+ def _write_state(agent_dir: Path, state: dict) -> None:
56
+ agent_dir.mkdir(parents=True, exist_ok=True)
57
+ tmp = agent_dir / f"{_STATE}.tmp"
58
+ tmp.write_text(json.dumps(state, indent=2), encoding="utf-8")
59
+ tmp.replace(agent_dir / _STATE)
60
+
61
+
62
+ def last_dream(agent_dir: Path) -> datetime:
63
+ """When the gates were last reset.
64
+
65
+ On the first check there is no record; that moment becomes the baseline, so
66
+ upgrading Nova does not set off a dream over months of old sessions at once.
67
+ """
68
+ state = _read_state(agent_dir)
69
+ try:
70
+ return datetime.fromisoformat(state["last_dream_at"])
71
+ except (KeyError, TypeError, ValueError):
72
+ now = _now()
73
+ _write_state(agent_dir, {**state, "last_dream_at": now.isoformat(), "trigger": "baseline"})
74
+ return now
75
+
76
+
77
+ def record_dream(agent_dir: Path, trigger: str, outcome: str) -> None:
78
+ """Reset the time and session gates after a dream finished."""
79
+ state = _read_state(agent_dir)
80
+ state.update(last_dream_at=_now().isoformat(), trigger=trigger, outcome=outcome)
81
+ _write_state(agent_dir, state)
82
+
83
+
84
+ def sessions_since(sessions_dir: Path, since: datetime) -> int:
85
+ """Sessions with at least one message started after ``since``."""
86
+ cutoff = since.timestamp()
87
+ count = 0
88
+ try:
89
+ entries = list(sessions_dir.iterdir())
90
+ except OSError:
91
+ return 0
92
+ for entry in entries:
93
+ try:
94
+ # A session untouched since the cutoff cannot have started after it;
95
+ # this skips reading most of a large history's meta files.
96
+ if not entry.is_dir() or entry.stat().st_mtime < cutoff:
97
+ continue
98
+ meta = json.loads((entry / "meta.json").read_text(encoding="utf-8"))
99
+ started = datetime.fromisoformat(meta["created_at"])
100
+ except (OSError, ValueError, KeyError, TypeError):
101
+ continue
102
+ if started > since and int(meta.get("message_count") or 0) > 0:
103
+ count += 1
104
+ return count
105
+
106
+
107
+ @dataclass(frozen=True)
108
+ class GateReport:
109
+ """Each gate's state, for the decision and for ``/dream status``."""
110
+
111
+ since: datetime
112
+ sessions: int
113
+ lock_holder: str | None # "pid 1234 since 10:02", or None when free
114
+
115
+ @property
116
+ def time_ok(self) -> bool:
117
+ """At least :data:`TIME_GATE` since the last dream."""
118
+ return _now() - self.since >= TIME_GATE
119
+
120
+ @property
121
+ def sessions_ok(self) -> bool:
122
+ """At least :data:`SESSION_GATE` sessions since the last dream."""
123
+ return self.sessions >= SESSION_GATE
124
+
125
+ @property
126
+ def lock_ok(self) -> bool:
127
+ """No other dream holds the lock."""
128
+ return self.lock_holder is None
129
+
130
+ @property
131
+ def open(self) -> bool:
132
+ """All three gates pass: an automatic dream may run."""
133
+ return self.time_ok and self.sessions_ok and self.lock_ok
134
+
135
+
136
+ def check_gates(agent_dir: Path, sessions_dir: Path) -> GateReport:
137
+ """Evaluate the three gates (reads files; call off the UI thread)."""
138
+ since = last_dream(agent_dir)
139
+ return GateReport(
140
+ since=since,
141
+ sessions=sessions_since(sessions_dir, since),
142
+ lock_holder=_lock_holder(agent_dir),
143
+ )
144
+
145
+
146
+ def _lock_holder(agent_dir: Path) -> str | None:
147
+ """Who holds a live lock, or None when it is free or stale."""
148
+ try:
149
+ info = json.loads((agent_dir / _LOCK).read_text(encoding="utf-8"))
150
+ taken = datetime.fromisoformat(info["at"])
151
+ except (OSError, ValueError, KeyError, TypeError):
152
+ return None
153
+ if _now() - taken > STALE_LOCK:
154
+ return None
155
+ return f"pid {info.get('pid')} since {taken.astimezone().strftime('%H:%M')}"
156
+
157
+
158
+ class DreamLock:
159
+ """The lock gate: an exclusive lock file; ``acquire()`` returns False if held."""
160
+
161
+ def __init__(self, agent_dir: Path) -> None: # noqa: D107
162
+ self._path = agent_dir / _LOCK
163
+ self._held = False
164
+
165
+ def acquire(self) -> bool:
166
+ """Take the lock, replacing a stale one; False when another dream holds it."""
167
+ self._path.parent.mkdir(parents=True, exist_ok=True)
168
+ for _ in range(2):
169
+ try:
170
+ fd = os.open(self._path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
171
+ except FileExistsError:
172
+ if _lock_holder(self._path.parent) is not None:
173
+ return False
174
+ # Stale (or unreadable): remove and retry once. A racing process
175
+ # that wins the retry keeps the lock; we then report it held.
176
+ try:
177
+ self._path.unlink()
178
+ except OSError:
179
+ return False
180
+ continue
181
+ with os.fdopen(fd, "w", encoding="utf-8") as stream:
182
+ json.dump({"pid": os.getpid(), "at": _now().isoformat()}, stream)
183
+ self._held = True
184
+ return True
185
+ return False
186
+
187
+ def release(self) -> None:
188
+ """Give the lock back (only if this object took it)."""
189
+ if self._held:
190
+ self._held = False
191
+ with contextlib.suppress(OSError):
192
+ self._path.unlink()
@@ -0,0 +1,59 @@
1
+ """Shared limits for the memory tiers.
2
+
3
+ Single source of truth for the per-file character budget used on both the
4
+ **write** side (Semantic-tier compaction in ``hermes/memory_tiers.py``) and the
5
+ **read** side (prompt-injection truncation in ``memory/agent_memory.py``). These
6
+ were previously two independent ``MAX_MEMORY_CHARS = 12_000`` constants that
7
+ could silently drift.
8
+
9
+ Compaction invariant (keep-newest):
10
+ Memory files are written newest-first (new sections/lessons are prepended,
11
+ above older ones). Both compaction and injection-time truncation therefore
12
+ keep the **head** of the file — which is the most recent content. Any new
13
+ memory writer MUST prepend, not append, to preserve this.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ # Maximum characters per memory file before disk compaction / truncation
19
+ # (~3,000 tokens at 4 chars/token). This is the WRITE-side cap only: it bounds
20
+ # what survives on disk, and is deliberately larger than the injection budget
21
+ # below so raising the budget later can still recover content.
22
+ MAX_MEMORY_CHARS = 12_000
23
+
24
+
25
+ #: Share of the context window ONE injected memory block may occupy. Four
26
+ #: blocks are injected every turn (agent.md, INDEX.md, HABITS.md, project
27
+ #: memory), so the tier as a whole is capped at ~12% of the window.
28
+ MEMORY_WINDOW_FRACTION = 0.03
29
+
30
+
31
+ #: Default per-block *injection* budget (agent.md, INDEX, HABITS, project
32
+ #: memory). Lowered from the 12k disk cap: the injected head is what the model
33
+ #: reads every turn, and older content is reachable on demand via
34
+ #: ``memory_search``. User-overridable in Nova.config.json
35
+ #: (``memory_block_chars``).
36
+ DEFAULT_MEMORY_BLOCK_CHARS = 6_000
37
+
38
+
39
+ #: Chars of the topic-memory INDEX injected into the system prompt. The index is
40
+ #: a pointer list (newest first); the rest is reachable via ``memory_search``, so
41
+ #: a small slice is enough to orient without paying for ~100k chars of pointers.
42
+ DEFAULT_MEMORY_INDEX_CHARS = 2_000
43
+
44
+
45
+ def memory_budget(context_window: int, cap: int = DEFAULT_MEMORY_BLOCK_CHARS) -> int:
46
+ """Chars one injected memory block may use, given the model's window.
47
+
48
+ ``MAX_MEMORY_CHARS`` is a *disk* cap and is window-independent — but as an
49
+ injection cap it was the same 12k on a 200K Claude window and on a 40K
50
+ Ollama one, where the four blocks together could claim a third of the
51
+ window before the conversation started. Skills already budget this way
52
+ (``skills.refreshing_middleware.listing_budget``); memory now does too.
53
+
54
+ ``cap`` overrides the ceiling (the user-configurable per-block budget);
55
+ the window fraction still applies, so a small window is never overrun.
56
+ """
57
+ if context_window <= 0:
58
+ return cap
59
+ return max(2_000, min(cap, int(context_window * MEMORY_WINDOW_FRACTION * 4)))
@@ -0,0 +1,106 @@
1
+ """Parsed memory cache with entry and resident-size limits."""
2
+
3
+ # JSON and LangGraph interfaces carry heterogeneous values.
4
+ # ruff: noqa: ANN401
5
+ import hashlib
6
+ import sys
7
+ from collections import OrderedDict
8
+ from collections.abc import Iterator, Mapping
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+
13
+ def file_hash(path: Any) -> str:
14
+ """Hash current contents in bounded chunks regardless of timestamps."""
15
+ digest = hashlib.sha256()
16
+ with path.open("rb") as stream:
17
+ for chunk in iter(lambda: stream.read(64 * 1024), b""):
18
+ digest.update(chunk)
19
+ return digest.hexdigest()
20
+
21
+
22
+ def resident_size(value: Any) -> int:
23
+ """Count strings and token sets as well as the container holding them."""
24
+ if isinstance(value, (tuple, list, set, frozenset)):
25
+ return sys.getsizeof(value) + sum(resident_size(item) for item in value)
26
+ return sys.getsizeof(value)
27
+
28
+
29
+ class ParsedMemoryCache(OrderedDict):
30
+ """LRU; conservative accounting counts shared values for each entry."""
31
+
32
+ def __init__(self, max_entries: int = 512, max_bytes: int = 16 * 1024 * 1024) -> None:
33
+ """Initialize local state without acquiring invocation ownership."""
34
+ super().__init__()
35
+ self.max_entries, self.max_bytes = max_entries, max_bytes
36
+ self.resident_bytes = 0
37
+ self._sizes: dict[Any, int] = {}
38
+
39
+ def __setitem__(self, key: Any, value: Any) -> None:
40
+ """Insert a parsed entry and evict the least recently used entries."""
41
+ size = resident_size(key) + resident_size(value) + 256
42
+ if key in self:
43
+ self.resident_bytes -= self._sizes.pop(key)
44
+ super().__delitem__(key)
45
+ if size > self.max_bytes:
46
+ return
47
+ super().__setitem__(key, value)
48
+ self._sizes[key] = size
49
+ self.resident_bytes += size
50
+ while len(self) > self.max_entries or self.resident_bytes > self.max_bytes:
51
+ oldest, _ = super().popitem(last=False)
52
+ self.resident_bytes -= self._sizes.pop(oldest)
53
+
54
+ def __getitem__(self, key: Any) -> Any:
55
+ """Read an entry and mark it as most recently used."""
56
+ value = super().__getitem__(key)
57
+ self.move_to_end(key)
58
+ return value
59
+
60
+ def __delitem__(self, key: Any) -> None:
61
+ """Delete a parsed entry and its resident-size accounting."""
62
+ super().__delitem__(key)
63
+ self.resident_bytes -= self._sizes.pop(key)
64
+
65
+ def clear(self) -> None:
66
+ """Release entries and their resident-size accounting together."""
67
+ super().clear()
68
+ self._sizes.clear()
69
+ self.resident_bytes = 0
70
+
71
+
72
+ class MemoryCorpus(Mapping):
73
+ """Keep discovery order while caching parsed bodies independently of corpus size."""
74
+
75
+ def __init__(self, cache: ParsedMemoryCache, tokenize: Any) -> None:
76
+ """Retain source descriptors; parse individual entries when retrieved."""
77
+ self.cache, self.tokenize = cache, tokenize
78
+ self.sources: dict[str, tuple[Any, Any, str]] = {}
79
+
80
+ def add(self, topic: str, title: Any, source: Path | str, digest: str) -> None:
81
+ """Register a source without admitting its body into the cache."""
82
+ self.sources[topic] = (title, source, digest)
83
+
84
+ def __len__(self) -> int:
85
+ """Count discoverable topics, including uncached topics."""
86
+ return len(self.sources)
87
+
88
+ def __iter__(self) -> Iterator[str]:
89
+ """Keep the original source ordering."""
90
+ return iter(self.sources)
91
+
92
+ def __getitem__(self, topic: str) -> Any:
93
+ """Parse on demand and retain only bodies that fit the bounded LRU."""
94
+ title, source, digest = self.sources[topic]
95
+ key = (topic, str(source) if isinstance(source, Path) else "inline", digest)
96
+ if key in self.cache:
97
+ return self.cache[key]
98
+ try:
99
+ body = source.read_text(encoding="utf-8") if isinstance(source, Path) else source
100
+ except OSError:
101
+ # A file can disappear between discovery and retrieval. Do not
102
+ # retain a negative parse; the next discovery sees the deletion.
103
+ return title, "", frozenset()
104
+ value = title, body, self.tokenize(body)
105
+ self.cache[key] = value
106
+ return value
@@ -0,0 +1,97 @@
1
+ """Which lessons get recalled, and whether recalling them helped.
2
+
3
+ Memory that is only ever written to grows without limit and nobody can tell the
4
+ useful part from the rest. This keeps two counts per lesson:
5
+
6
+ * ``recalls`` — how many times it was put in front of the model;
7
+ * ``resolved`` — how many of those were at a failure that was followed by a
8
+ non-failure. That is a weak signal (the next command may have succeeded for
9
+ any reason), but a lesson recalled twenty times at errors that never clear is
10
+ not earning its place, and one that is never recalled at all is dead weight.
11
+
12
+ Nothing here deletes anything: :func:`unused` and :func:`report` say what the
13
+ evidence is, and a person or a curation pass decides.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import hashlib
19
+ import json
20
+ import logging
21
+ import time
22
+ from pathlib import Path
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ _FILE = ".recall-stats.json"
27
+
28
+
29
+ def _key(bullet: str) -> str:
30
+ text = " ".join(bullet.lstrip("-*• ").split()).lower()
31
+ return hashlib.sha1(text.encode("utf-8")).hexdigest()[:16] # noqa: S324 — an id, not security
32
+
33
+
34
+ def _path(agent_dir: Path) -> Path:
35
+ return agent_dir / "memories" / _FILE
36
+
37
+
38
+ def _read(agent_dir: Path) -> dict[str, dict]:
39
+ try:
40
+ data = json.loads(_path(agent_dir).read_text(encoding="utf-8"))
41
+ except (OSError, ValueError):
42
+ return {}
43
+ return data if isinstance(data, dict) else {}
44
+
45
+
46
+ def _write(agent_dir: Path, stats: dict[str, dict]) -> None:
47
+ try:
48
+ path = _path(agent_dir)
49
+ path.parent.mkdir(parents=True, exist_ok=True)
50
+ path.write_text(json.dumps(stats, indent=1), encoding="utf-8")
51
+ except OSError:
52
+ logger.debug("could not write recall stats", exc_info=True)
53
+
54
+
55
+ def note_recalled(agent_dir: Path, bullets: list[str]) -> list[str]:
56
+ """Count one recall for each bullet; returns their keys."""
57
+ if not bullets:
58
+ return []
59
+ stats, now, keys = _read(agent_dir), time.time(), []
60
+ for bullet in bullets:
61
+ key = _key(bullet)
62
+ entry = stats.setdefault(
63
+ key, {"text": bullet.lstrip("-*• ").strip()[:160], "recalls": 0, "resolved": 0, "first": now}
64
+ )
65
+ entry["recalls"] += 1
66
+ entry["last"] = now
67
+ keys.append(key)
68
+ _write(agent_dir, stats)
69
+ return keys
70
+
71
+
72
+ def note_resolved(agent_dir: Path, keys: list[str]) -> None:
73
+ """The failure these lessons were recalled at was followed by a non-failure."""
74
+ if not keys:
75
+ return
76
+ stats = _read(agent_dir)
77
+ for key in keys:
78
+ if key in stats:
79
+ stats[key]["resolved"] = stats[key].get("resolved", 0) + 1
80
+ _write(agent_dir, stats)
81
+
82
+
83
+ def report(agent_dir: Path) -> list[dict]:
84
+ """Every tracked lesson, most recalled first."""
85
+ return sorted(_read(agent_dir).values(), key=lambda e: -e.get("recalls", 0))
86
+
87
+
88
+ def unused(agent_dir: Path, lessons: list[str], *, older_than_days: float = 30.0) -> list[str]:
89
+ """Lessons that exist but have never been recalled, among ``lessons``.
90
+
91
+ ``older_than_days`` cannot be checked for a lesson with no recall record (it
92
+ has no timestamp here), so callers pass only lessons they know to be at
93
+ least that old — typically from the topic file's own review timestamps.
94
+ """
95
+ del older_than_days # documented contract; ageing is the caller's knowledge
96
+ seen = _read(agent_dir)
97
+ return [bullet for bullet in lessons if _key(bullet) not in seen]
@@ -0,0 +1,116 @@
1
+ """Meaning-level similarity between short texts, computed locally.
2
+
3
+ Lesson handling used to compare words: is this bullet a repeat of that one, has
4
+ another project recorded the same fact, does this lesson bear on the error on
5
+ screen. Word overlap got each of those wrong on real data — it missed four of
6
+ five same-fact pairs, "confirmed" a toolchain note because it shared the phrase
7
+ ``apt-get update``, and could not connect ``python3: command not found`` to a
8
+ lesson that only described the fix.
9
+
10
+ This uses the static embedding model Nova already depends on for code search
11
+ (``potion-code-16M`` via ``model2vec``): no network or API call per use, ~1 s to
12
+ load once, and a few hundred texts embed in ~20 ms. If it cannot be loaded every
13
+ caller falls back to word overlap, so nothing depends on it being present.
14
+
15
+ Measured on lessons from Terminal-Bench runs (cosine):
16
+ same fact reworded 0.59-0.81; different facts on one subject 0.34-0.47 (one pair
17
+ 0.65); unrelated ~0.0; the error a lesson is about 0.50-0.70; ordinary output
18
+ against every lesson under 0.2.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import logging
24
+ import os
25
+ import threading
26
+ from contextlib import suppress
27
+ from typing import Any
28
+
29
+ from novacode_cli.computation_cache import digest
30
+ from novacode_cli.embeddings import get_model
31
+ from novacode_cli.memory.parsed_cache import ParsedMemoryCache
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ #: Two bullets at or above this say the same thing.
36
+ SAME_FACT = 0.66
37
+ #: A lesson at or above this bears on the text it is compared with.
38
+ RELEVANT = 0.45
39
+
40
+ _CACHE_LIMIT = 4096
41
+ _cache = ParsedMemoryCache(max_entries=_CACHE_LIMIT, max_bytes=8 * 1024 * 1024)
42
+ _cache_model: Any = None
43
+ _embed_lock = threading.Lock()
44
+
45
+
46
+ def enabled() -> bool:
47
+ """False when switched off by ``NOVA_SEMANTIC_MEMORY=0`` or the model is missing."""
48
+ if os.environ.get("NOVA_SEMANTIC_MEMORY", "").strip().lower() in {"0", "false", "off"}:
49
+ return False
50
+ return _load() is not None
51
+
52
+
53
+ def _load() -> Any: # noqa: ANN401
54
+ return get_model()
55
+
56
+
57
+ def embed(texts: list[str]) -> Any: # noqa: ANN401
58
+ """Unit-length vectors for ``texts`` (rows), or None when unavailable."""
59
+ if not texts or not enabled():
60
+ return None
61
+ import numpy as np
62
+
63
+ global _cache_model # noqa: PLW0603
64
+ model = _load()
65
+ cache_enabled = os.environ.get("NOVA_DISABLE_COMPUTATION_CACHE") != "1"
66
+ # Concurrent memory searches share one miss computation. The lock also
67
+ # protects LRU promotion and eviction; no network calls happen in encode.
68
+ with _embed_lock:
69
+ if _cache_model is not model:
70
+ _cache.clear()
71
+ _cache_model = model
72
+ unique = dict.fromkeys(texts)
73
+ keys = {text: digest(text) for text in unique}
74
+ found = {}
75
+ for text in unique:
76
+ if cache_enabled:
77
+ with suppress(KeyError):
78
+ found[text] = _cache[keys[text]]
79
+ missing = [text for text in unique if text not in found]
80
+ if missing:
81
+ try:
82
+ vectors = model.encode(missing)
83
+ except Exception: # noqa: BLE001
84
+ logger.debug("embedding failed", exc_info=True)
85
+ return None
86
+ norms = np.linalg.norm(vectors, axis=1, keepdims=True)
87
+ vectors = vectors / np.where(norms == 0, 1.0, norms)
88
+ for text, vector in zip(missing, vectors, strict=True):
89
+ # A row view would retain the entire batch allocation even if
90
+ # every other row was evicted, defeating the byte budget.
91
+ found[text] = vector.copy()
92
+ if cache_enabled:
93
+ _cache[keys[text]] = found[text]
94
+ return np.stack([found[text] for text in texts])
95
+
96
+
97
+ def best_match(text: str, candidates: list[str]) -> tuple[float, int]:
98
+ """``(similarity, index)`` of the candidate closest to ``text``; ``(0.0, -1)`` if none."""
99
+ if not text.strip() or not candidates:
100
+ return 0.0, -1
101
+ vectors = embed([text, *candidates])
102
+ if vectors is None:
103
+ return 0.0, -1
104
+ scores = vectors[1:] @ vectors[0]
105
+ index = int(scores.argmax())
106
+ return float(scores[index]), index
107
+
108
+
109
+ def similarities(text: str, candidates: list[str]) -> list[float]:
110
+ """Similarity of ``text`` to each candidate; empty when unavailable."""
111
+ if not text.strip() or not candidates:
112
+ return []
113
+ vectors = embed([text, *candidates])
114
+ if vectors is None:
115
+ return []
116
+ return [float(s) for s in vectors[1:] @ vectors[0]]