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,894 @@
1
+ """Optimized FilesystemBackend wrapping deepagents.backends.filesystem.
2
+
3
+ Improvements over the upstream :class:`FilesystemBackend`, all delivered as
4
+ method overrides (the deepagents package is never modified — the filesystem
5
+ tools call ``backend.read/edit/glob/grep``, so overriding those backend methods
6
+ improves the tools transparently):
7
+
8
+ 1. **grep hang fix** — the upstream Python fallback walks the tree with
9
+ ``rglob("*")``, which cannot skip large, uninteresting directories (``.venv``,
10
+ ``.git``, ``node_modules``). We override ``_python_search`` to use ``os.walk``
11
+ with in-place ``dirnames`` pruning so those subtrees are never descended into.
12
+
13
+ 2. **Regex support** — ``grep()`` gains an opt-in ``use_regex`` flag, fully
14
+ backward-compatible with the literal default.
15
+
16
+ 3. **Smart-case grep** — a lower-case pattern matches case-insensitively; a
17
+ pattern with any upper-case letter stays case-sensitive (ripgrep's default).
18
+
19
+ 4. **read encoding recovery** — ``read()`` falls back to ``utf-8-sig`` / cp1252 /
20
+ latin-1 when a file isn't utf-8, instead of returning a decode error.
21
+
22
+ 5. **edit failure hints** — a failed ``edit()`` gets an actionable hint (closest
23
+ line for a miss, replace_all/disambiguation tip for an ambiguous match).
24
+
25
+ 6. **glob prune + ordering** — ``glob()`` drops vendored-dir noise and returns
26
+ newest-first.
27
+
28
+ Async tool calls (``aread``/``aedit``/``aglob``/``agrep``) delegate to these
29
+ sync methods via ``asyncio.to_thread`` in the protocol base, so they are covered
30
+ too without overriding the async variants.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import functools
36
+ import inspect
37
+ import json
38
+ import os
39
+ import re
40
+ import shutil
41
+ import subprocess
42
+ import time
43
+ from pathlib import Path
44
+
45
+ import wcmatch.glob as wcglob
46
+ from deepagents.backends.filesystem import FilesystemBackend
47
+ from deepagents.backends.local_shell import LocalShellBackend
48
+ from deepagents.backends.protocol import EditResult, GlobResult, GrepResult, ReadResult, WriteResult
49
+
50
+ from novacode_cli.core.tool_results import current_read_capture
51
+
52
+ __all__ = ["OptimizedFilesystemBackend", "OptimizedLocalShellBackend"]
53
+
54
+ # Wall-clock budget for a single grep, in seconds. Defined locally rather than
55
+ # imported from deepagents.backends.protocol: the constant only exists in
56
+ # deepagents>=0.6.8, but Nova is launched against environments still on 0.6.7
57
+ # (e.g. a miniconda install). The value matches upstream's default.
58
+ _GREP_TIMEOUT = 30
59
+ # deepagents' read error for an offset past the end of the file.
60
+ _PAST_END = re.compile(r"exceeds file length \((\d+) lines\)")
61
+
62
+
63
+ @functools.cache
64
+ def _resolve_ripgrep_path() -> str | None:
65
+ r"""Locate the ``rg`` executable, more thoroughly than ``shutil.which``.
66
+
67
+ The upstream resolver only checks ``PATH``. On Windows, package managers like
68
+ winget install ripgrep as a loose archive and never link it onto ``PATH``
69
+ (e.g. ``...\\WinGet\\Packages\\BurntSushi.ripgrep...\\rg.exe``), so the agent
70
+ silently falls back to the slow Python search even though ripgrep is present.
71
+
72
+ Resolution order:
73
+ 1. ``NOVA_RIPGREP_PATH`` / ``RIPGREP_PATH`` env override (explicit wins).
74
+ 2. ``shutil.which`` (honours ``PATH`` — the fast, normal case).
75
+ 3. Known package-manager install locations (winget, VS Code bundle, cargo,
76
+ scoop, chocolatey), globbed for the versioned subdirectories they use.
77
+
78
+ Cached for the process lifetime; ``cache_clear()`` re-probes.
79
+ """
80
+ for env_var in ("NOVA_RIPGREP_PATH", "RIPGREP_PATH"):
81
+ override = os.environ.get(env_var)
82
+ if override and Path(override).is_file():
83
+ return override
84
+
85
+ for name in ("rg", "rg.exe"):
86
+ found = shutil.which(name)
87
+ if found:
88
+ return found
89
+
90
+ # Glob patterns for package managers that don't touch PATH. Each may contain
91
+ # a version directory, so we glob and take the newest match.
92
+ local = os.environ.get("LOCALAPPDATA", "")
93
+ userprofile = os.environ.get("USERPROFILE", "")
94
+ programdata = os.environ.get("PROGRAMDATA", "")
95
+ candidates: list[str] = []
96
+ if local:
97
+ candidates += [
98
+ # winget loose-archive install
99
+ rf"{local}\Microsoft\WinGet\Packages\BurntSushi.ripgrep*\**\rg.exe",
100
+ # VS Code bundled ripgrep
101
+ rf"{local}\Programs\Microsoft VS Code\resources\app\node_modules*\**\rg.exe",
102
+ ]
103
+ if userprofile:
104
+ candidates += [
105
+ rf"{userprofile}\.cargo\bin\rg.exe",
106
+ rf"{userprofile}\scoop\shims\rg.exe",
107
+ rf"{userprofile}\scoop\apps\ripgrep\**\rg.exe",
108
+ ]
109
+ if programdata:
110
+ candidates.append(rf"{programdata}\chocolatey\bin\rg.exe")
111
+
112
+ newest: tuple[float, str] | None = None
113
+ for pattern in candidates:
114
+ for match in _glob_abs(pattern):
115
+ try:
116
+ if not match.is_file():
117
+ continue
118
+ mtime = match.stat().st_mtime
119
+ except OSError:
120
+ continue
121
+ if newest is None or mtime > newest[0]:
122
+ newest = (mtime, str(match))
123
+ return newest[1] if newest else None
124
+
125
+
126
+ def _glob_abs(windows_pattern: str) -> list[Path]:
127
+ r"""Glob an absolute Windows pattern by anchoring at its drive root.
128
+
129
+ ``Path.glob`` rejects absolute/drive-anchored patterns, so split off the
130
+ anchor (e.g. ``C:\\``) and glob the remainder against it:
131
+ ``C:\\a\\**\\rg.exe`` → ``Path("C:\\").glob("a/**/rg.exe")``.
132
+ """
133
+ p = Path(windows_pattern)
134
+ anchor = p.anchor
135
+ if not anchor:
136
+ return list(Path().glob(str(p).replace("\\", "/")))
137
+ rel = str(p)[len(anchor) :].replace("\\", "/")
138
+ try:
139
+ return list(Path(anchor).glob(rel))
140
+ except (OSError, ValueError):
141
+ return []
142
+
143
+
144
+ # Directory names pruned during the Python grep fallback. These are virtually
145
+ # always gitignored and can each hold millions of files; descending into them is
146
+ # what made the fallback hang. An explicit search *into* one of these (e.g.
147
+ # path="/node_modules") still works — only nested occurrences are skipped.
148
+ _SKIP_DIRS = frozenset(
149
+ {
150
+ ".venv",
151
+ ".env",
152
+ "venv",
153
+ ".git",
154
+ ".hg",
155
+ ".svn",
156
+ "node_modules",
157
+ "__pycache__",
158
+ ".pytest_cache",
159
+ ".mypy_cache",
160
+ ".ruff_cache",
161
+ "dist",
162
+ "build",
163
+ ".next",
164
+ ".turbo",
165
+ ".cargo",
166
+ "target",
167
+ "out",
168
+ ".vscode",
169
+ ".idea",
170
+ }
171
+ )
172
+
173
+
174
+ # deepagents changed its internal grep contract in 0.7.0, and pyproject allows
175
+ # both (``deepagents>=0.6.8``):
176
+ #
177
+ # 0.6.10: _ripgrep_search(pattern, base, glob) -> dict | None
178
+ # _python_search(pattern, base, glob, *, timeout) -> (dict, error)
179
+ # 0.7.0: _ripgrep_search(pattern, base, glob, max_count) -> (dict | None, truncated)
180
+ # _python_search(..., *, max_count, timeout) -> (dict, truncated, error)
181
+ #
182
+ # The parent's own ``grep()`` unpacks whichever shape ITS version expects, so an
183
+ # override that returns only one shape breaks the other install ("'tuple' object
184
+ # has no attribute 'items'" on 0.6.x). Detect the installed contract once and
185
+ # have the overrides return the matching shape.
186
+ _PARENT_RG_V2 = "max_count" in inspect.signature(FilesystemBackend._ripgrep_search).parameters
187
+ _PARENT_PY_V2 = "max_count" in inspect.signature(FilesystemBackend._python_search).parameters
188
+
189
+
190
+ def _cap_results(
191
+ results: dict[str, list[tuple[int, str]]], max_count: int | None
192
+ ) -> tuple[dict[str, list[tuple[int, str]]], bool]:
193
+ """Trim *results* to at most ``max_count`` matches, reporting truncation.
194
+
195
+ Mirrors the upstream grep contract: the caller gets exactly ``max_count``
196
+ matches and ``truncated=True`` only when matches were actually dropped.
197
+ """
198
+ if max_count is None or max_count < 0:
199
+ return results, False
200
+ kept = 0
201
+ capped: dict[str, list[tuple[int, str]]] = {}
202
+ truncated = False
203
+ for fpath, items in results.items():
204
+ if kept >= max_count:
205
+ truncated = True
206
+ break
207
+ room = max_count - kept
208
+ capped[fpath] = items[:room]
209
+ if len(items) > room:
210
+ truncated = True
211
+ kept += len(capped[fpath])
212
+ return capped, truncated
213
+
214
+
215
+ def _decode_text_bytes(raw: bytes) -> str:
216
+ """Decode file bytes without depending on the platform's locale codec.
217
+
218
+ ``Path.read_text()`` uses the locale's preferred encoding, which is cp1252
219
+ on Windows: a UTF-8 file then fails to decode and the whole file is skipped
220
+ (silently losing matches). Try utf-8 first, then the same ladder the
221
+ ``read`` path uses; latin-1 always succeeds, so a search never dies on an
222
+ undecodable byte.
223
+ """
224
+ for enc in ("utf-8-sig", "cp1252"):
225
+ try:
226
+ return raw.decode(enc)
227
+ except (UnicodeDecodeError, LookupError):
228
+ continue
229
+ return raw.decode("latin-1", errors="replace")
230
+
231
+
232
+ class OptimizedFilesystemBackend(FilesystemBackend):
233
+ """FilesystemBackend with a non-hanging grep fallback and regex support."""
234
+
235
+ def write(self, file_path: str, content: str) -> WriteResult:
236
+ """Keep agent-authored skills discoverable in supported skill libraries."""
237
+ # Check only actual installed skill paths, never arbitrary SKILL.md
238
+ # examples or repository source files. The parent still owns the write
239
+ # and its symlink/path protections; validation happens before mutation.
240
+ if Path(file_path).name == "SKILL.md":
241
+ try:
242
+ target = self._resolve_path(file_path)
243
+ library = target.parent.parent
244
+ if library.name == "skills" and library.parent.name in {
245
+ ".nova", ".agents", ".claude",
246
+ }:
247
+ from novacode_cli.skills.schema import prepare_skill_content
248
+
249
+ content = prepare_skill_content(content, target.parent.name)
250
+ except (OSError, RuntimeError, ValueError) as exc:
251
+ return WriteResult(error=f"Cannot save skill '{file_path}': {exc}")
252
+ return super().write(file_path, content)
253
+
254
+ def _to_virtual_path(self, path: str | Path) -> str:
255
+ """Map a real path to its virtual form, following symlinks/junctions.
256
+
257
+ The parent does ``path.resolve().relative_to(self.cwd)``. When a child is
258
+ a symlink or Windows junction whose *target* lives outside the root, that
259
+ raises ``ValueError`` and the entry is silently dropped from ``ls``/glob —
260
+ so a skill installed as ``~/.claude/skills/<name>`` junctioned to
261
+ ``~/.agents/skills/<name>`` never gets discovered (``/<name>`` command
262
+ "isn't a recognized command"). Claude Code follows these links; Nova must
263
+ too.
264
+
265
+ Fix: if the strict resolve escapes the root, fall back to the child's
266
+ *lexical* position — resolve the PARENT (normal chain) and re-attach the
267
+ child's own name. A genuine escape (the parent itself is outside root)
268
+ still raises, preserving the containment guard.
269
+ """
270
+ p = Path(path)
271
+ try:
272
+ return super()._to_virtual_path(p)
273
+ except ValueError:
274
+ # relative_to raises if the parent is also outside the root → genuine
275
+ # escape, let it propagate (excluded, as before).
276
+ rel = p.parent.resolve().relative_to(self.cwd)
277
+ return "/" + (rel / p.name).as_posix()
278
+
279
+ def _resolve_path(self, key: str) -> Path:
280
+ """Resolve an inbound path, following symlinks/junctions rooted in-tree.
281
+
282
+ The parent rejects any path whose *resolved* target escapes the root. That
283
+ also rejects a legitimate junction placed inside the root — e.g. a skill
284
+ installed as ``~/.claude/skills/<name>`` that points to
285
+ ``~/.agents/skills/<name>`` — so ``read``/``glob``/``download_files`` fail
286
+ for it and ``/<name>`` "isn't a recognized command".
287
+
288
+ Virtual paths already forbid ``..`` and ``~`` (below), so a virtual path
289
+ can never climb out of the root *lexically*; the resolved target can only
290
+ escape by following a symlink/junction that physically lives inside the
291
+ root — which is the intended skill-install layout, and what Claude Code
292
+ itself follows. So we keep the parent's traversal guard and the symlink-
293
+ loop guard, and drop only the resolved-target containment rejection.
294
+ Non-virtual mode is unchanged.
295
+ """
296
+ if not self.virtual_mode:
297
+ return super()._resolve_path(key)
298
+ from deepagents.backends.filesystem import _raise_if_symlink_loop
299
+
300
+ from novacode_cli.integrations.host_path import host_path_to_virtual
301
+ from novacode_cli.security.paths import virtual_relative_path
302
+
303
+ key = host_path_to_virtual(key, str(self.cwd))
304
+ full = (self.cwd / virtual_relative_path(key)).resolve()
305
+ _raise_if_symlink_loop(full)
306
+ return full
307
+
308
+ def grep(
309
+ self,
310
+ pattern: str,
311
+ path: str | None = None,
312
+ glob: str | None = None,
313
+ *,
314
+ use_regex: bool = False,
315
+ ) -> GrepResult:
316
+ """Search for a pattern in files.
317
+
318
+ Args:
319
+ pattern: Text to search for. Literal by default; a regular expression
320
+ when ``use_regex=True``.
321
+ path: Directory or file to search in. Defaults to the current directory.
322
+ glob: Optional glob to filter which files are searched (e.g. ``"*.py"``).
323
+ use_regex: If ``False`` (default) ``pattern`` is matched literally; if
324
+ ``True`` it is compiled as a Python regular expression.
325
+
326
+ Returns:
327
+ A ``GrepResult`` with matches or an error.
328
+ """
329
+ # Literal mode is the parent's exact behaviour — and since the parent
330
+ # calls self._ripgrep_search / self._python_search, it already picks up
331
+ # both of our overrides (hang fix included). No need to duplicate it.
332
+ if not use_regex:
333
+ return super().grep(pattern, path, glob)
334
+
335
+ # Regex mode: same orchestration as the parent, but the pattern is passed
336
+ # through unescaped to both ripgrep (no -F) and the Python fallback.
337
+ try:
338
+ base_full = self._resolve_path(path or ".")
339
+ if not base_full.exists():
340
+ return GrepResult(matches=[])
341
+ except ValueError:
342
+ return GrepResult(matches=[])
343
+ except (OSError, RuntimeError) as e:
344
+ return GrepResult(error=f"Error searching path '{path or '.'}': {e}", matches=[])
345
+
346
+ try:
347
+ re.compile(pattern)
348
+ except re.error as e:
349
+ return GrepResult(error=f"Invalid regex pattern: {e}", matches=[])
350
+
351
+ # Call the impls, not the version-adaptive overrides: these always return
352
+ # the rich tuple regardless of which deepagents is installed.
353
+ results, truncated = self._rg_impl(pattern, base_full, glob, use_regex=True)
354
+ partial_error: str | None = None
355
+ if results is None:
356
+ results, truncated, partial_error = self._py_impl(pattern, base_full, glob)
357
+
358
+ matches = [
359
+ {"path": fpath, "line": int(line_num), "text": line_text}
360
+ for fpath, items in results.items()
361
+ for line_num, line_text in items
362
+ ]
363
+ # GrepResult gained `truncated` in 0.7; 0.6.x rejects the kwarg.
364
+ if _PARENT_RG_V2:
365
+ return GrepResult(error=partial_error, matches=matches, truncated=truncated)
366
+ return GrepResult(error=partial_error, matches=matches)
367
+
368
+ def _ripgrep_search(
369
+ self,
370
+ pattern: str,
371
+ base_full: Path,
372
+ include_glob: str | None,
373
+ max_count: int | None = None,
374
+ *,
375
+ use_regex: bool = False,
376
+ ):
377
+ """Version-adaptive override the parent ``grep()`` calls.
378
+
379
+ Returns ``(results, truncated)`` on deepagents >= 0.7 and a bare
380
+ ``results`` dict on 0.6.x — matching whatever the installed parent
381
+ unpacks. Nova's own code should call :meth:`_rg_impl` instead, which
382
+ always returns the richer tuple.
383
+ """
384
+ out = self._rg_impl(pattern, base_full, include_glob, max_count, use_regex=use_regex)
385
+ return out if _PARENT_RG_V2 else out[0]
386
+
387
+ def _rg_impl( # noqa: PLR0912, PLR0915
388
+ self,
389
+ pattern: str,
390
+ base_full: Path,
391
+ include_glob: str | None,
392
+ max_count: int | None = None,
393
+ *,
394
+ use_regex: bool = False,
395
+ ) -> tuple[dict[str, list[tuple[int, str]]] | None, bool]:
396
+ """Search with ripgrep.
397
+
398
+ Identical to the upstream implementation except that the fixed-string
399
+ flag (``-F``) is omitted when ``use_regex`` is set, so the pattern is
400
+ interpreted as a regular expression. The parent's literal ``grep()`` call
401
+ passes no ``use_regex`` and therefore keeps ``-F``.
402
+
403
+ Returns ``(results, truncated)`` — matching the upstream contract, which
404
+ the parent ``grep()`` unpacks. ``results`` is ``None`` to signal "fall
405
+ back to the Python search" (ripgrep missing, timed out, or errored);
406
+ ``truncated`` is True when ``max_count`` capped the result set.
407
+ """
408
+ from novacode_cli.backends.native_search import ugrep_search
409
+
410
+ native = ugrep_search(
411
+ pattern, base_full, include_glob, use_regex=use_regex,
412
+ skip_dirs=_SKIP_DIRS, timeout=_GREP_TIMEOUT,
413
+ )
414
+ if native is not None:
415
+ if self.virtual_mode:
416
+ virtual = {}
417
+ for p, lines in native.items():
418
+ try:
419
+ virtual["/" + Path(p).relative_to(self.cwd).as_posix()] = lines
420
+ except ValueError:
421
+ continue
422
+ native = virtual
423
+ return _cap_results(native, max_count)
424
+ rg_path = _resolve_ripgrep_path()
425
+ if rg_path is None:
426
+ return None, False
427
+
428
+ # --smart-case: a lower-case pattern matches case-insensitively, but a
429
+ # pattern containing an upper-case letter stays case-sensitive. Matches
430
+ # ripgrep's CLI default and the Python fallback below, so literal grep is
431
+ # forgiving for the common lower-case query without losing precision.
432
+ cmd = [rg_path, "--json", "--smart-case"]
433
+ if not use_regex:
434
+ cmd.append("-F") # fixed-string (literal) mode
435
+ if include_glob:
436
+ cmd.extend(["--glob", include_glob])
437
+
438
+ rg_cwd: str | None = None
439
+ if base_full.is_dir():
440
+ cmd.extend(["--", pattern, "."])
441
+ rg_cwd = str(base_full)
442
+ else:
443
+ cmd.extend(["--", pattern, str(base_full)])
444
+
445
+ try:
446
+ proc = subprocess.run( # noqa: S603
447
+ cmd,
448
+ capture_output=True,
449
+ # ripgrep emits UTF-8. Without an explicit encoding Python decodes
450
+ # with the locale codec (cp1252 on Windows), and one non-ASCII
451
+ # character in a matched line raises UnicodeDecodeError inside the
452
+ # reader thread: the search then dies, or silently returns nothing.
453
+ text=True,
454
+ encoding="utf-8",
455
+ errors="replace",
456
+ timeout=_GREP_TIMEOUT,
457
+ check=False,
458
+ cwd=rg_cwd,
459
+ )
460
+ except subprocess.TimeoutExpired:
461
+ return None, False
462
+ except (FileNotFoundError, PermissionError, NotADirectoryError):
463
+ # rg resolved at cache time but failed to exec — re-probe next call.
464
+ _resolve_ripgrep_path.cache_clear()
465
+ return None, False
466
+
467
+ # 0 = match, 1 = no match (both fine); 2+ = hard error → fall back.
468
+ if proc.returncode not in (0, 1):
469
+ return None, False
470
+
471
+ results: dict[str, list[tuple[int, str]]] = {}
472
+ base_resolved = base_full.resolve()
473
+ # On Windows, proc.stdout can be None even with a 0/1 returncode.
474
+ # Treat that as "no matches" rather than crashing on splitlines().
475
+ if not proc.stdout:
476
+ return results, False
477
+ for line in proc.stdout.splitlines():
478
+ try:
479
+ data = json.loads(line)
480
+ except json.JSONDecodeError:
481
+ continue
482
+ if data.get("type") != "match":
483
+ continue
484
+ pdata = data.get("data", {})
485
+ ftext = pdata.get("path", {}).get("text")
486
+ if not ftext:
487
+ continue
488
+ raw = Path(ftext)
489
+ p = raw if raw.is_absolute() else (base_full / raw)
490
+ # Containment guard: drop any result that resolves outside the root.
491
+ try:
492
+ resolved = p.resolve()
493
+ resolved.relative_to(base_resolved)
494
+ except (ValueError, OSError):
495
+ continue
496
+ if self.virtual_mode:
497
+ try:
498
+ # Already canonicalized for containment above. Resolving it
499
+ # again performs another Windows filesystem round trip per
500
+ # match; derive the identical virtual path from this result.
501
+ virt = "/" + resolved.relative_to(self.cwd).as_posix()
502
+ except (ValueError, OSError, RuntimeError):
503
+ continue
504
+ else:
505
+ virt = str(p)
506
+ ln = pdata.get("line_number")
507
+ if ln is None:
508
+ continue
509
+ lt = pdata.get("lines", {}).get("text", "").rstrip("\n")
510
+ results.setdefault(virt, []).append((int(ln), lt))
511
+
512
+ # ponytail: cap applied after collection, not by killing rg mid-stream as
513
+ # upstream does. Same visible contract (exactly max_count, flagged
514
+ # truncated); only the peak-memory bound is looser. Stream it if a huge
515
+ # match set ever actually hurts.
516
+ return _cap_results(results, max_count)
517
+
518
+ def _python_search(
519
+ self,
520
+ pattern: str,
521
+ base_full: Path,
522
+ include_glob: str | None,
523
+ *,
524
+ max_count: int | None = None,
525
+ timeout: int = _GREP_TIMEOUT,
526
+ ):
527
+ """Version-adaptive override the parent ``grep()`` calls.
528
+
529
+ ``(results, truncated, error)`` on deepagents >= 0.7, ``(results, error)``
530
+ on 0.6.x. Nova's own code should call :meth:`_py_impl`.
531
+ """
532
+ results, truncated, error = self._py_impl(
533
+ re.escape(pattern), base_full, include_glob, max_count=max_count, timeout=timeout
534
+ )
535
+ return (results, truncated, error) if _PARENT_PY_V2 else (results, error)
536
+
537
+ def _py_impl( # noqa: PLR0912
538
+ self,
539
+ pattern: str,
540
+ base_full: Path,
541
+ include_glob: str | None,
542
+ *,
543
+ max_count: int | None = None,
544
+ timeout: int = _GREP_TIMEOUT,
545
+ ) -> tuple[dict[str, list[tuple[int, str]]], bool, str | None]:
546
+ """Python fallback that prunes large directories to avoid hanging.
547
+
548
+ Drop-in replacement for the upstream ``_python_search`` (same signature,
549
+ so the parent ``grep()`` calls this automatically). The only behavioural
550
+ difference is that subtrees in :data:`_SKIP_DIRS` are never descended
551
+ into, which is what kept the upstream version from completing on repos
552
+ with a vendored ``.venv`` / ``node_modules``.
553
+
554
+ Args:
555
+ pattern: Regex pattern. Callers pass ``re.escape(...)`` for a literal
556
+ search (the parent does this) or a raw regex for regex mode.
557
+ base_full: Resolved base path to search.
558
+ include_glob: Optional glob to filter files.
559
+ max_count: Optional cap on total matches collected.
560
+ timeout: Wall-clock budget in seconds.
561
+
562
+ Returns:
563
+ ``(results, truncated, partial_error)`` — the upstream contract the
564
+ parent ``grep()`` unpacks. ``truncated`` is True when ``max_count``
565
+ dropped matches; ``partial_error`` is set when the walk was cut short
566
+ by the timeout or aborted mid-iteration.
567
+ """
568
+ deadline = time.monotonic() + timeout
569
+ # Smart-case to mirror the ripgrep path: case-insensitive unless the
570
+ # pattern carries an upper-case letter. `pattern` is already re.escape'd
571
+ # for literal searches, which never adds upper-case, so the check is safe.
572
+ smart_flags = re.IGNORECASE if pattern == pattern.lower() else 0
573
+ regex = re.compile(pattern, smart_flags)
574
+ results: dict[str, list[tuple[int, str]]] = {}
575
+ root = base_full if base_full.is_dir() else base_full.parent
576
+
577
+ def _timed_out() -> str:
578
+ return (
579
+ f"Grep of '{base_full}' timed out after {timeout}s with "
580
+ f"{len(results)} matching file(s); try a more specific pattern "
581
+ f"or a narrower path."
582
+ )
583
+
584
+ # Collect one past the cap so truncation can be reported exactly (a run
585
+ # that lands on max_count with nothing left is complete, not truncated).
586
+ probe = None if max_count is None else max_count + 1
587
+ matched = 0
588
+
589
+ try:
590
+ walk = (
591
+ [(str(root), [], [base_full.name])] if base_full.is_file()
592
+ else os.walk(str(root))
593
+ )
594
+ for dirpath, dirnames, filenames in walk:
595
+ if probe is not None and matched >= probe:
596
+ break
597
+ if time.monotonic() > deadline:
598
+ capped, trunc = _cap_results(results, max_count)
599
+ return capped, trunc, _timed_out()
600
+
601
+ # Prune in place so os.walk never descends into skipped subtrees.
602
+ dirnames[:] = [
603
+ d for d in dirnames if d not in _SKIP_DIRS and not d.endswith(".egg-info")
604
+ ]
605
+
606
+ for filename in filenames:
607
+ if probe is not None and matched >= probe:
608
+ break
609
+ if time.monotonic() > deadline:
610
+ capped, trunc = _cap_results(results, max_count)
611
+ return capped, trunc, _timed_out()
612
+
613
+ fp = Path(dirpath) / filename
614
+
615
+ if include_glob:
616
+ rel_path = fp.relative_to(root).as_posix()
617
+ if not wcglob.globmatch(
618
+ rel_path, include_glob,
619
+ flags=wcglob.BRACE | wcglob.GLOBSTAR | wcglob.MATCHBASE,
620
+ ):
621
+ continue
622
+
623
+ try:
624
+ if fp.stat().st_size > self.max_file_size_bytes:
625
+ continue
626
+ except (OSError, RuntimeError):
627
+ continue
628
+
629
+ try:
630
+ content = _decode_text_bytes(fp.read_bytes())
631
+ except (PermissionError, OSError, RuntimeError):
632
+ continue
633
+
634
+ if "\x00" in content:
635
+ continue
636
+
637
+ for line_num, line in enumerate(content.splitlines(), 1):
638
+ if probe is not None and matched >= probe:
639
+ break
640
+ if not regex.search(line):
641
+ continue
642
+ if self.virtual_mode:
643
+ try:
644
+ virt_path = self._to_virtual_path(fp)
645
+ except (ValueError, OSError, RuntimeError):
646
+ continue
647
+ else:
648
+ virt_path = str(fp)
649
+ results.setdefault(virt_path, []).append((line_num, line))
650
+ matched += 1
651
+
652
+ except (OSError, RuntimeError) as e:
653
+ msg = f"Grep of '{base_full}' aborted after {len(results)} matching file(s): {e}"
654
+ capped, trunc = _cap_results(results, max_count)
655
+ return capped, trunc, msg
656
+
657
+ capped, trunc = _cap_results(results, max_count)
658
+ return capped, trunc, None
659
+
660
+ # -- read: recover non-utf-8 files ---------------------------------------
661
+
662
+ def read(self, file_path: str, offset: int = 0, limit: int = 2000) -> ReadResult:
663
+ """Read a file, recovering from a utf-8 decode failure.
664
+
665
+ Upstream opens text files as utf-8 only, so a cp1252 / latin-1 / UTF-16
666
+ file returns a decode error and reads *nothing*. On that error we re-read
667
+ the bytes through a short encoding ladder (``latin-1`` always succeeds),
668
+ returning the same windowed ``ReadResult`` the tool expects.
669
+ """
670
+ result = super().read(file_path, offset, limit)
671
+ if result.error and "codec can't decode" in result.error:
672
+ recovered = self._read_fallback_encoding(file_path, offset, limit)
673
+ if recovered is not None:
674
+ result = recovered
675
+ if result.error and (past_end := _PAST_END.search(result.error)):
676
+ # Say what to do next: a bare "exceeds file length" costs a turn guessing.
677
+ total = int(past_end.group(1))
678
+ last_page = max(0, total - limit)
679
+ result = ReadResult(
680
+ error=(
681
+ f"{result.error}. The file ends at line {total}; read the last page "
682
+ f"with offset={last_page}, or start again with offset=0."
683
+ )
684
+ )
685
+ capture = current_read_capture.get()
686
+ if capture is not None:
687
+ capture.record(result)
688
+ return result
689
+
690
+ def _read_fallback_encoding(
691
+ self, file_path: str, offset: int, limit: int
692
+ ) -> ReadResult | None:
693
+ """Re-read ``file_path`` trying non-utf-8 encodings; ``None`` if all fail."""
694
+ try:
695
+ raw = self._resolve_path(file_path).read_bytes()
696
+ except (OSError, RuntimeError, ValueError):
697
+ return None
698
+ for enc in ("utf-8-sig", "cp1252", "latin-1"):
699
+ try:
700
+ content = raw.decode(enc)
701
+ except (UnicodeDecodeError, LookupError):
702
+ continue
703
+ lines = content.splitlines(keepends=True)
704
+ if lines and offset >= len(lines):
705
+ return ReadResult(
706
+ error=f"Line offset {offset} exceeds file length ({len(lines)} lines)"
707
+ )
708
+ end = min(offset + limit, len(lines))
709
+ windowed = "".join(lines[offset:end])
710
+ return ReadResult(file_data={"content": windowed, "encoding": "utf-8"})
711
+ return None
712
+
713
+ # -- edit: actionable failure feedback -----------------------------------
714
+
715
+ def edit(
716
+ self,
717
+ file_path: str,
718
+ old_string: str,
719
+ new_string: str,
720
+ replace_all: bool = False, # noqa: FBT001, FBT002
721
+ ) -> EditResult:
722
+ """Edit a file, adding a recovery hint when the match fails.
723
+
724
+ The upstream error ("String not found" / "appears N times") tells the
725
+ model *that* it failed but not how to fix it. We append a hint — the
726
+ closest line for a miss, or a replace_all/disambiguation tip for an
727
+ ambiguous match — so the next attempt usually lands.
728
+ """
729
+ result = super().edit(file_path, old_string, new_string, replace_all)
730
+ if not result.error:
731
+ return result
732
+ hint = self._edit_failure_hint(file_path, old_string, result.error)
733
+ if hint:
734
+ return EditResult(error=f"{result.error}\n\n{hint}")
735
+ return result
736
+
737
+ def _edit_failure_hint(self, file_path: str, old_string: str, error: str) -> str | None:
738
+ """Build a one-line recovery hint for a failed edit (``None`` if none)."""
739
+ low = error.lower()
740
+ if "appears" in low and "times" in low:
741
+ return (
742
+ "Hint: pass replace_all=True to change every occurrence, or extend "
743
+ "old_string with surrounding lines so it matches exactly one place."
744
+ )
745
+ if "not found" not in low:
746
+ return None
747
+ try:
748
+ content = self._resolve_path(file_path).read_text(encoding="utf-8", errors="replace")
749
+ except (OSError, RuntimeError, ValueError):
750
+ return None
751
+ first = next((ln for ln in old_string.splitlines() if ln.strip()), "")
752
+ needle = re.sub(r"\s+", " ", first).strip()
753
+ if not needle:
754
+ return None
755
+ # Whitespace-insensitive locate: indentation / internal-spacing drift is
756
+ # the most common reason an otherwise-correct old_string doesn't match.
757
+ for i, line in enumerate(content.splitlines(), 1):
758
+ norm = re.sub(r"\s+", " ", line).strip()
759
+ if norm and needle in norm:
760
+ return (
761
+ f"Hint: no exact match, but line {i} is close:\n {i}: {line}\n"
762
+ "old_string must match byte-for-byte (indentation, quotes, trailing "
763
+ f"spaces). Re-read around line {i} and copy the exact text."
764
+ )
765
+ return None
766
+
767
+ # -- glob: prune noise during the walk + newest-first ---------------------
768
+
769
+ def glob(self, pattern: str, path: str | None = None) -> GlobResult:
770
+ """Glob with vendored dirs pruned *during* the walk, newest-first.
771
+
772
+ Upstream calls ``search_path.rglob(pattern)``, which descends into every
773
+ subtree and ``stat()``s every hit before anything can be filtered. On a
774
+ repo with a vendored ``.venv`` / ``node_modules`` that is tens of
775
+ thousands of files (26k here vs 1.8k real ones), which blows the 20s
776
+ ``GLOB_TIMEOUT`` in deepagents' glob tool.
777
+
778
+ Filtering afterwards, as this method used to, still paid the full cost.
779
+ We walk with ``os.walk`` and prune :data:`_SKIP_DIRS` from ``dirnames``
780
+ in place, so those subtrees are never descended into — the same fix
781
+ :meth:`_py_impl` already applies to grep. Results are ordered by mtime so
782
+ the files the user is likely working on come first.
783
+ """
784
+ if self.virtual_mode and ".." in Path(pattern).parts:
785
+ msg = "Path traversal not allowed in glob pattern"
786
+ raise ValueError(msg)
787
+
788
+ try:
789
+ search_path = self.cwd if path in (None, "/") else self._resolve_path(path)
790
+ if not search_path.exists() or not search_path.is_dir():
791
+ return GlobResult(matches=[])
792
+ except (OSError, RuntimeError, ValueError) as e:
793
+ return GlobResult(error=f"Error globbing path '{path}': {e}", matches=[])
794
+
795
+ # rglob(p) matches p at any depth; wcglob needs that spelled out.
796
+ match_pat = pattern.lstrip("/")
797
+ if not match_pat.startswith("**/"):
798
+ match_pat = "**/" + match_pat
799
+ flags = wcglob.BRACE | wcglob.GLOBSTAR
800
+
801
+ matches: list[dict] = []
802
+ try:
803
+ from novacode_cli.backends.native_search import bfs_files
804
+
805
+ native_files = bfs_files(search_path, _SKIP_DIRS)
806
+ walk = (
807
+ [(str(p.parent), [], [p.name]) for p in native_files]
808
+ if native_files is not None else os.walk(str(search_path))
809
+ )
810
+ for dirpath, dirnames, filenames in walk:
811
+ dirnames[:] = [
812
+ d for d in dirnames if d not in _SKIP_DIRS and not d.endswith(".egg-info")
813
+ ]
814
+ for name in filenames:
815
+ full = Path(dirpath) / name
816
+ try:
817
+ rel = full.relative_to(search_path).as_posix()
818
+ except ValueError:
819
+ continue
820
+ if not wcglob.globmatch(rel, match_pat, flags=flags):
821
+ continue
822
+ if self.virtual_mode:
823
+ try:
824
+ out_path = self._to_virtual_path(full)
825
+ except (OSError, RuntimeError, ValueError):
826
+ continue
827
+ else:
828
+ out_path = str(full)
829
+ try:
830
+ st = full.stat()
831
+ matches.append(
832
+ {
833
+ "path": out_path,
834
+ "is_dir": False,
835
+ "size": int(st.st_size),
836
+ "_mtime": st.st_mtime,
837
+ }
838
+ )
839
+ except OSError:
840
+ matches.append({"path": out_path, "is_dir": False, "_mtime": 0.0})
841
+ except (OSError, RuntimeError, ValueError) as e:
842
+ return GlobResult(
843
+ error=f"Glob of '{path}' aborted partway: {e}",
844
+ matches=[{k: v for k, v in m.items() if k != "_mtime"} for m in matches],
845
+ )
846
+
847
+ matches.sort(key=lambda m: m["_mtime"], reverse=True)
848
+ for m in matches:
849
+ del m["_mtime"]
850
+ return GlobResult(matches=matches)
851
+
852
+
853
+ class ConversationHistoryBackend(FilesystemBackend):
854
+ """Write-only store for conversation history evicted by summarization.
855
+
856
+ The summarization middleware offloads replaced history here and tells the
857
+ model it can "recover the full text by reading the offloaded file". Acting on
858
+ that advice re-inhales the exact context eviction just cleared, which pushes
859
+ usage back over the summarization threshold and summarizes again — the
860
+ repeating "SESSION INTENT / task not recoverable" loop.
861
+
862
+ Writes still land on disk, so the transcript stays available to the user and
863
+ to `/resume`; only the agent-facing *read* is refused, which is what breaks
864
+ the cycle. Deterministic on purpose: a prompt-level "don't re-read this" can
865
+ be ignored by the model, a refused read cannot.
866
+ """
867
+
868
+ def read(self, file_path: str, offset: int = 0, limit: int = 2000) -> ReadResult:
869
+ """Refuse the read with a short instruction instead of the transcript."""
870
+ return ReadResult(
871
+ error=(
872
+ "This file holds earlier conversation history that was already "
873
+ "summarized out of the active context. Re-reading it would undo "
874
+ "that compaction and re-trigger summarization, so it is not "
875
+ "readable. Continue from the summary you have; if the goal is "
876
+ "unclear, ask the user rather than trying to reconstruct it."
877
+ )
878
+ )
879
+
880
+
881
+ class OptimizedLocalShellBackend(LocalShellBackend, OptimizedFilesystemBackend):
882
+ """A ``LocalShellBackend`` that uses Nova's guarded, non-hanging grep.
883
+
884
+ Nova's local-mode composite default backend must support the ``execute``
885
+ tool (so subagents can run commands), which is why deepagents'
886
+ :class:`LocalShellBackend` is used. But ``LocalShellBackend`` subclasses the
887
+ *base* deepagents ``FilesystemBackend``, whose ``_ripgrep_search`` lacks the
888
+ Windows None-stdout guard and whose Python fallback walks ``.venv`` /
889
+ ``node_modules`` (the hang this package fixes). The MRO here —
890
+ ``LocalShellBackend`` → :class:`OptimizedFilesystemBackend` → ``FilesystemBackend``
891
+ — keeps ``execute`` from the former while resolving ``grep`` /
892
+ ``_ripgrep_search`` to the optimized, guarded versions. No new behaviour of
893
+ its own.
894
+ """