alysis-code 0.13.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 (458) hide show
  1. alysis_code/__init__.py +3 -0
  2. alysis_code/__main__.py +4 -0
  3. alysis_code/_build_info.py +14 -0
  4. alysis_code/account_login.py +468 -0
  5. alysis_code/agent/README.md +35 -0
  6. alysis_code/agent/__init__.py +11 -0
  7. alysis_code/agent/acceptance_contract.py +2217 -0
  8. alysis_code/agent/blast_radius.py +1403 -0
  9. alysis_code/agent/cache_keepalive.py +227 -0
  10. alysis_code/agent/completion_certificate.py +366 -0
  11. alysis_code/agent/completion_gate.py +306 -0
  12. alysis_code/agent/empty_response_stall.py +403 -0
  13. alysis_code/agent/errors.py +28 -0
  14. alysis_code/agent/llm_calls.py +475 -0
  15. alysis_code/agent/mutation_classification.py +227 -0
  16. alysis_code/agent/prompt_context.py +2508 -0
  17. alysis_code/agent/read_ledger.py +253 -0
  18. alysis_code/agent/regression_baseline.py +642 -0
  19. alysis_code/agent/reproduction_first.py +610 -0
  20. alysis_code/agent/sensitive_output.py +629 -0
  21. alysis_code/agent/session.py +3218 -0
  22. alysis_code/agent/steering.py +191 -0
  23. alysis_code/agent/subagent_execution.py +5177 -0
  24. alysis_code/agent/subagent_workspace.py +666 -0
  25. alysis_code/agent/tools_assembly.py +4728 -0
  26. alysis_code/agent/turn/__init__.py +101 -0
  27. alysis_code/agent/turn/core.py +8483 -0
  28. alysis_code/agent/turn/events.py +113 -0
  29. alysis_code/agent/turn/exploration.py +590 -0
  30. alysis_code/agent/turn/interventions.py +65 -0
  31. alysis_code/agent/turn/read_cache.py +420 -0
  32. alysis_code/agent/turn/snapshot.py +179 -0
  33. alysis_code/agent/turn_contract.py +661 -0
  34. alysis_code/agent/turn_path.py +129 -0
  35. alysis_code/agent/verification.py +2885 -0
  36. alysis_code/agent/verification_commands.py +512 -0
  37. alysis_code/agent/verification_evidence.py +738 -0
  38. alysis_code/agent_loop.py +693 -0
  39. alysis_code/agent_runtimes/__init__.py +51 -0
  40. alysis_code/agent_runtimes/base.py +114 -0
  41. alysis_code/agent_runtimes/builtins.py +129 -0
  42. alysis_code/agent_runtimes/codex_cli.py +664 -0
  43. alysis_code/agent_runtimes/host.py +263 -0
  44. alysis_code/agent_runtimes/registry.py +64 -0
  45. alysis_code/agent_runtimes/service.py +150 -0
  46. alysis_code/agentbox_client.py +416 -0
  47. alysis_code/agentbox_integration.py +310 -0
  48. alysis_code/alysis_cloud.py +152 -0
  49. alysis_code/approval_scope.py +276 -0
  50. alysis_code/assets/README.md +33 -0
  51. alysis_code/assets/__init__.py +126 -0
  52. alysis_code/assets/asset_read_core.py +281 -0
  53. alysis_code/assets/budget_allocator.py +456 -0
  54. alysis_code/assets/comprehender.py +759 -0
  55. alysis_code/assets/index.py +654 -0
  56. alysis_code/assets/ingestion.py +275 -0
  57. alysis_code/assets/legacy_migration.py +413 -0
  58. alysis_code/assets/models.py +263 -0
  59. alysis_code/assets/ocr.py +239 -0
  60. alysis_code/assets/owl/ascii/f-000.txt +13 -0
  61. alysis_code/assets/owl/ascii/f-001.txt +13 -0
  62. alysis_code/assets/owl/ascii/f-002.txt +13 -0
  63. alysis_code/assets/owl/ascii/f-003.txt +13 -0
  64. alysis_code/assets/owl/ascii/f-004.txt +13 -0
  65. alysis_code/assets/owl/ascii/f-005.txt +13 -0
  66. alysis_code/assets/owl/ascii/f-006.txt +13 -0
  67. alysis_code/assets/owl/ascii/f-007.txt +13 -0
  68. alysis_code/assets/owl/ascii/f-008.txt +13 -0
  69. alysis_code/assets/owl/ascii/f-009.txt +13 -0
  70. alysis_code/assets/owl/ascii/f-010.txt +13 -0
  71. alysis_code/assets/owl/ascii/f-011.txt +13 -0
  72. alysis_code/assets/owl/ascii/f-012.txt +13 -0
  73. alysis_code/assets/owl/ascii/f-013.txt +13 -0
  74. alysis_code/assets/owl/ascii/f-014.txt +13 -0
  75. alysis_code/assets/owl/ascii/f-015.txt +13 -0
  76. alysis_code/assets/owl/ascii/f-016.txt +13 -0
  77. alysis_code/assets/owl/ascii/f-017.txt +13 -0
  78. alysis_code/assets/owl/ascii/f-018.txt +13 -0
  79. alysis_code/assets/owl/ascii/f-019.txt +13 -0
  80. alysis_code/assets/owl/ascii/f-020.txt +13 -0
  81. alysis_code/assets/owl/index.html +98 -0
  82. alysis_code/assets/owl/show-owl.sh +761 -0
  83. alysis_code/assets/paths.py +49 -0
  84. alysis_code/assets/plan_binding.py +326 -0
  85. alysis_code/assets/planner_context.py +466 -0
  86. alysis_code/assets/planner_tools.py +184 -0
  87. alysis_code/assets/prompts.py +101 -0
  88. alysis_code/assets/replanner_context.py +239 -0
  89. alysis_code/assets/surface.py +521 -0
  90. alysis_code/assets/untrusted_content.py +48 -0
  91. alysis_code/assets/usage_logger.py +94 -0
  92. alysis_code/assets/worker_mirror.py +428 -0
  93. alysis_code/assets/worker_section.py +303 -0
  94. alysis_code/assets/worker_tools.py +468 -0
  95. alysis_code/atomic_io.py +83 -0
  96. alysis_code/auth_diagnostics.py +272 -0
  97. alysis_code/background_runner.py +366 -0
  98. alysis_code/branding.py +270 -0
  99. alysis_code/budget_policy.py +390 -0
  100. alysis_code/build_identity.py +465 -0
  101. alysis_code/builtin_hooks/__init__.py +7 -0
  102. alysis_code/builtin_hooks/notify_done_windows.py +65 -0
  103. alysis_code/bwrap_etc.py +76 -0
  104. alysis_code/cancellation.py +41 -0
  105. alysis_code/capabilities.py +137 -0
  106. alysis_code/chatgpt_codex_static_provider.py +133 -0
  107. alysis_code/cli.py +51 -0
  108. alysis_code/cli_impl/__init__.py +1 -0
  109. alysis_code/cli_impl/assets_cli.py +537 -0
  110. alysis_code/cli_impl/assets_modal.py +412 -0
  111. alysis_code/cli_impl/chat/__init__.py +156 -0
  112. alysis_code/cli_impl/chat/commands.py +2616 -0
  113. alysis_code/cli_impl/chat/loop.py +4508 -0
  114. alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
  115. alysis_code/cli_impl/chat/rendering.py +444 -0
  116. alysis_code/cli_impl/chat/state.py +124 -0
  117. alysis_code/cli_impl/chat_resume.py +830 -0
  118. alysis_code/cli_impl/chat_slash_completer.py +258 -0
  119. alysis_code/cli_impl/commands/__init__.py +11 -0
  120. alysis_code/cli_impl/commands/_shared.py +89 -0
  121. alysis_code/cli_impl/commands/auth.py +623 -0
  122. alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
  123. alysis_code/cli_impl/commands/chat_state.py +158 -0
  124. alysis_code/cli_impl/commands/chat_status.py +1248 -0
  125. alysis_code/cli_impl/commands/chat_terminal.py +942 -0
  126. alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
  127. alysis_code/cli_impl/commands/cli_common.py +1223 -0
  128. alysis_code/cli_impl/commands/cli_surface.py +77 -0
  129. alysis_code/cli_impl/commands/config.py +131 -0
  130. alysis_code/cli_impl/commands/conventions.py +85 -0
  131. alysis_code/cli_impl/commands/execution_helpers.py +350 -0
  132. alysis_code/cli_impl/commands/extensions.py +401 -0
  133. alysis_code/cli_impl/commands/forge.py +1282 -0
  134. alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
  135. alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
  136. alysis_code/cli_impl/commands/hooks.py +737 -0
  137. alysis_code/cli_impl/commands/ide_bridge.py +31 -0
  138. alysis_code/cli_impl/commands/mcp.py +700 -0
  139. alysis_code/cli_impl/commands/profile.py +453 -0
  140. alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
  141. alysis_code/cli_impl/commands/report.py +88 -0
  142. alysis_code/cli_impl/commands/root.py +1118 -0
  143. alysis_code/cli_impl/commands/sandbox.py +184 -0
  144. alysis_code/cli_impl/commands/server.py +54 -0
  145. alysis_code/cli_impl/commands/sessions.py +252 -0
  146. alysis_code/cli_impl/commands/skills.py +404 -0
  147. alysis_code/cli_impl/commands/startup.py +946 -0
  148. alysis_code/cli_impl/commands/tools.py +335 -0
  149. alysis_code/cli_impl/commands/update.py +364 -0
  150. alysis_code/cli_impl/commands/welcome.py +972 -0
  151. alysis_code/cli_impl/config_menu.py +3882 -0
  152. alysis_code/cli_impl/forge.py +4509 -0
  153. alysis_code/cli_impl/forge_recovery.py +485 -0
  154. alysis_code/cli_impl/setup_wizard.py +2409 -0
  155. alysis_code/cli_impl/tui/__init__.py +58 -0
  156. alysis_code/cli_impl/tui/app.py +4551 -0
  157. alysis_code/cli_impl/tui/config.py +32 -0
  158. alysis_code/cli_impl/tui/config_flow.py +2754 -0
  159. alysis_code/cli_impl/tui/config_overlay.py +566 -0
  160. alysis_code/cli_impl/tui/content.py +78 -0
  161. alysis_code/cli_impl/tui/footer.py +218 -0
  162. alysis_code/cli_impl/tui/forge_status.py +136 -0
  163. alysis_code/cli_impl/tui/markdown.py +244 -0
  164. alysis_code/cli_impl/tui/owl.py +109 -0
  165. alysis_code/cli_impl/tui/plan_meta.py +477 -0
  166. alysis_code/cli_impl/tui/setup_app.py +519 -0
  167. alysis_code/cli_impl/tui/setup_flow.py +1622 -0
  168. alysis_code/cli_impl/tui/state.py +101 -0
  169. alysis_code/cli_impl/tui/subagent_identity.py +66 -0
  170. alysis_code/cli_impl/tui/subagent_panel.py +186 -0
  171. alysis_code/cli_impl/tui/surface.py +796 -0
  172. alysis_code/cli_impl/tui/transcript.py +514 -0
  173. alysis_code/cli_impl/tui/update_prompt.py +79 -0
  174. alysis_code/cli_impl/tui/workspace_guard.py +384 -0
  175. alysis_code/clipboard.py +172 -0
  176. alysis_code/code_review.py +1211 -0
  177. alysis_code/compaction/__init__.py +28 -0
  178. alysis_code/compaction/conversation_compactor.py +2932 -0
  179. alysis_code/compaction/importance.py +177 -0
  180. alysis_code/compaction/settings.py +297 -0
  181. alysis_code/compaction/tool_output_offload.py +447 -0
  182. alysis_code/config.py +3509 -0
  183. alysis_code/conflict_auto_resolver.py +895 -0
  184. alysis_code/context/__init__.py +1 -0
  185. alysis_code/context/tool_schema_budgeter.py +220 -0
  186. alysis_code/crash_diagnostics.py +282 -0
  187. alysis_code/custom_tools/README.md +34 -0
  188. alysis_code/custom_tools/__init__.py +43 -0
  189. alysis_code/custom_tools/discovery.py +903 -0
  190. alysis_code/custom_tools/runtime.py +1516 -0
  191. alysis_code/custom_tools/session.py +227 -0
  192. alysis_code/custom_tools/trust.py +232 -0
  193. alysis_code/diff_paths.py +113 -0
  194. alysis_code/direction_change.py +293 -0
  195. alysis_code/dispatch_timing.py +306 -0
  196. alysis_code/durable_service_manager.py +1236 -0
  197. alysis_code/edit_discipline.py +659 -0
  198. alysis_code/error_text.py +73 -0
  199. alysis_code/execution_budget.py +411 -0
  200. alysis_code/execution_context.py +915 -0
  201. alysis_code/execution_deadline.py +1065 -0
  202. alysis_code/execution_shared.py +1904 -0
  203. alysis_code/extensions/README.md +30 -0
  204. alysis_code/extensions/__init__.py +93 -0
  205. alysis_code/extensions/activation.py +138 -0
  206. alysis_code/extensions/install.py +1436 -0
  207. alysis_code/extensions/manifest.py +487 -0
  208. alysis_code/extensions/models.py +74 -0
  209. alysis_code/extensions/paths.py +56 -0
  210. alysis_code/extensions/registry.json +4 -0
  211. alysis_code/extensions/registry.py +52 -0
  212. alysis_code/extensions/state.py +83 -0
  213. alysis_code/extensions/workspace_trust.py +101 -0
  214. alysis_code/failed_task_evidence.py +369 -0
  215. alysis_code/failure_category.py +315 -0
  216. alysis_code/feedback_report.py +1647 -0
  217. alysis_code/file_classification.py +485 -0
  218. alysis_code/forge.py +2064 -0
  219. alysis_code/forge_completion.py +362 -0
  220. alysis_code/forge_events.py +475 -0
  221. alysis_code/frontmatter_utils.py +95 -0
  222. alysis_code/git_evidence.py +1181 -0
  223. alysis_code/git_ops.py +560 -0
  224. alysis_code/git_safe.py +62 -0
  225. alysis_code/git_worktrees.py +190 -0
  226. alysis_code/hooks/README.md +33 -0
  227. alysis_code/hooks/__init__.py +67 -0
  228. alysis_code/hooks/audit.py +171 -0
  229. alysis_code/hooks/config.py +225 -0
  230. alysis_code/hooks/dispatcher.py +1110 -0
  231. alysis_code/hooks/models.py +447 -0
  232. alysis_code/hooks/trust.py +202 -0
  233. alysis_code/host_actions.py +543 -0
  234. alysis_code/host_browser.py +103 -0
  235. alysis_code/ide/__init__.py +5 -0
  236. alysis_code/ide/activity_events.py +399 -0
  237. alysis_code/ide/approvals.py +337 -0
  238. alysis_code/ide/artifacts.py +153 -0
  239. alysis_code/ide/browser_egress_proxy.py +1076 -0
  240. alysis_code/ide/cdp_websocket_transport.py +1192 -0
  241. alysis_code/ide/change_ledger.py +1721 -0
  242. alysis_code/ide/context_blocks.py +979 -0
  243. alysis_code/ide/event_stream.py +531 -0
  244. alysis_code/ide/forge_protocol.py +3112 -0
  245. alysis_code/ide/forge_request_ledger.py +737 -0
  246. alysis_code/ide/health.py +965 -0
  247. alysis_code/ide/managed_browser.py +2251 -0
  248. alysis_code/ide/management_protocol.py +3414 -0
  249. alysis_code/ide/mcp_oauth_coordinator.py +744 -0
  250. alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
  251. alysis_code/ide/prompt_queue.py +1070 -0
  252. alysis_code/ide/protocol.py +191 -0
  253. alysis_code/ide/resumable_swarm.py +1543 -0
  254. alysis_code/ide/session_search.py +295 -0
  255. alysis_code/ide/stdio_bridge.py +9935 -0
  256. alysis_code/ide/structured_state.py +1579 -0
  257. alysis_code/ide/swarm_protocol.py +816 -0
  258. alysis_code/integration_gate.py +506 -0
  259. alysis_code/interactive_input_guard.py +39 -0
  260. alysis_code/interactive_plan_mode.py +26 -0
  261. alysis_code/internal_artifacts.py +179 -0
  262. alysis_code/knowledge_base.py +1409 -0
  263. alysis_code/knowledge_capture.py +1190 -0
  264. alysis_code/knowledge_librarian.py +605 -0
  265. alysis_code/language_policy.py +34 -0
  266. alysis_code/litellm_static_provider.py +535 -0
  267. alysis_code/llm/__init__.py +1 -0
  268. alysis_code/llm/anthropic_messages.py +2288 -0
  269. alysis_code/llm/base.py +71 -0
  270. alysis_code/llm/cache_capabilities.py +985 -0
  271. alysis_code/llm/cache_control_blocks.py +244 -0
  272. alysis_code/llm/cache_policy.py +388 -0
  273. alysis_code/llm/factory.py +373 -0
  274. alysis_code/llm/gemini_generate_content.py +2652 -0
  275. alysis_code/llm/gemini_interactions.py +739 -0
  276. alysis_code/llm/metadata.py +450 -0
  277. alysis_code/llm/openai_compat.py +2947 -0
  278. alysis_code/llm/openai_responses.py +2604 -0
  279. alysis_code/llm/protocols.py +609 -0
  280. alysis_code/llm/provider_limits.py +525 -0
  281. alysis_code/llm/request_plan.py +389 -0
  282. alysis_code/llm/request_shape.py +238 -0
  283. alysis_code/llm/streaming.py +108 -0
  284. alysis_code/llm/temperature_compat.py +78 -0
  285. alysis_code/llm/types.py +195 -0
  286. alysis_code/llm/usage_normalization.py +222 -0
  287. alysis_code/llm_error_display.py +315 -0
  288. alysis_code/logging_redaction.py +326 -0
  289. alysis_code/managed_host_deadline.py +196 -0
  290. alysis_code/mcp/README.md +33 -0
  291. alysis_code/mcp/__init__.py +24 -0
  292. alysis_code/mcp/client.py +1137 -0
  293. alysis_code/mcp/config.py +597 -0
  294. alysis_code/mcp/errors.py +113 -0
  295. alysis_code/mcp/forge_scope.py +154 -0
  296. alysis_code/mcp/jsonrpc.py +214 -0
  297. alysis_code/mcp/manager.py +2308 -0
  298. alysis_code/mcp/models.py +666 -0
  299. alysis_code/mcp/oauth.py +972 -0
  300. alysis_code/mcp/oauth_runtime.py +310 -0
  301. alysis_code/mcp/oauth_store.py +276 -0
  302. alysis_code/mcp/prompts.py +329 -0
  303. alysis_code/mcp/resources.py +295 -0
  304. alysis_code/mcp/roots.py +106 -0
  305. alysis_code/mcp/server_requests.py +75 -0
  306. alysis_code/mcp/token_store.py +859 -0
  307. alysis_code/mcp/transport_http.py +1338 -0
  308. alysis_code/mcp/transport_stdio.py +1267 -0
  309. alysis_code/mcp/untrusted_content.py +119 -0
  310. alysis_code/merge_conflict_reviewer.py +729 -0
  311. alysis_code/model_catalog/__init__.py +1 -0
  312. alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
  313. alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
  314. alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
  315. alysis_code/model_metadata_policy.py +223 -0
  316. alysis_code/model_metadata_utils.py +103 -0
  317. alysis_code/model_registry.py +1420 -0
  318. alysis_code/model_router.py +147 -0
  319. alysis_code/permission_policy.py +1016 -0
  320. alysis_code/personas.py +451 -0
  321. alysis_code/pipeline_facts.py +233 -0
  322. alysis_code/plan_assistant.py +4763 -0
  323. alysis_code/plan_mode.py +393 -0
  324. alysis_code/plan_reconciliation.py +1228 -0
  325. alysis_code/plan_repair.py +652 -0
  326. alysis_code/plan_validation.py +1099 -0
  327. alysis_code/planning_constraints.py +904 -0
  328. alysis_code/policy.py +95 -0
  329. alysis_code/preview_server.py +457 -0
  330. alysis_code/process_reaping.py +566 -0
  331. alysis_code/profile_presets.py +1834 -0
  332. alysis_code/profiles.py +666 -0
  333. alysis_code/provider_auth/__init__.py +29 -0
  334. alysis_code/provider_auth/base.py +99 -0
  335. alysis_code/provider_auth/openai_codex.py +951 -0
  336. alysis_code/provider_auth/registry.py +76 -0
  337. alysis_code/provider_auth/store.py +125 -0
  338. alysis_code/provider_diagnostics.py +1209 -0
  339. alysis_code/provider_model_catalog.py +685 -0
  340. alysis_code/provider_telemetry.py +1699 -0
  341. alysis_code/provider_url.py +75 -0
  342. alysis_code/reasoning_contracts.py +911 -0
  343. alysis_code/remote_sync.py +350 -0
  344. alysis_code/replanning.py +1195 -0
  345. alysis_code/repo_scan.py +1152 -0
  346. alysis_code/request_estimation.py +296 -0
  347. alysis_code/review_gate.py +617 -0
  348. alysis_code/run_lock.py +1141 -0
  349. alysis_code/run_outcome.py +58 -0
  350. alysis_code/run_provenance.py +774 -0
  351. alysis_code/run_state.py +445 -0
  352. alysis_code/runtime_artifacts.py +116 -0
  353. alysis_code/runtime_context_features.py +78 -0
  354. alysis_code/runtime_kind.py +52 -0
  355. alysis_code/safety/__init__.py +11 -0
  356. alysis_code/safety/mcp_sanitize.py +29 -0
  357. alysis_code/safety/safe_http.py +297 -0
  358. alysis_code/safety/subagent_report.py +184 -0
  359. alysis_code/sandbox_doctor.py +682 -0
  360. alysis_code/sandbox_runner.py +1025 -0
  361. alysis_code/sandbox_settings.py +423 -0
  362. alysis_code/serialized_paths.py +355 -0
  363. alysis_code/server/__init__.py +3 -0
  364. alysis_code/server/app.py +367 -0
  365. alysis_code/server/auth.py +34 -0
  366. alysis_code/server/job_config.py +30 -0
  367. alysis_code/server/settings.py +215 -0
  368. alysis_code/server/store.py +193 -0
  369. alysis_code/server/worker_runner.py +657 -0
  370. alysis_code/service_persistence.py +355 -0
  371. alysis_code/session_artifacts.py +108 -0
  372. alysis_code/session_metrics.py +331 -0
  373. alysis_code/session_store.py +624 -0
  374. alysis_code/skills/README.md +34 -0
  375. alysis_code/skills/__init__.py +104 -0
  376. alysis_code/skills/conventions.py +84 -0
  377. alysis_code/skills/discovery.py +176 -0
  378. alysis_code/skills/eval_models.py +232 -0
  379. alysis_code/skills/eval_runner.py +372 -0
  380. alysis_code/skills/evals.py +1344 -0
  381. alysis_code/skills/install.py +293 -0
  382. alysis_code/skills/loader.py +118 -0
  383. alysis_code/skills/matching.py +103 -0
  384. alysis_code/skills/models.py +71 -0
  385. alysis_code/skills/paths.py +56 -0
  386. alysis_code/skills/prompting.py +500 -0
  387. alysis_code/skills/scaffold.py +142 -0
  388. alysis_code/skills/state.py +441 -0
  389. alysis_code/skills/transactions.py +125 -0
  390. alysis_code/skills/validation.py +304 -0
  391. alysis_code/step_budget.py +238 -0
  392. alysis_code/subagent_labels.py +49 -0
  393. alysis_code/subagents.py +1072 -0
  394. alysis_code/surface/__init__.py +80 -0
  395. alysis_code/surface/base.py +305 -0
  396. alysis_code/surface/console.py +387 -0
  397. alysis_code/surface/events.py +372 -0
  398. alysis_code/surface/hidden_surface.py +529 -0
  399. alysis_code/surface/noop_surface.py +219 -0
  400. alysis_code/surface/rich_surface.py +1555 -0
  401. alysis_code/surface/styles.py +67 -0
  402. alysis_code/surface/theme.py +455 -0
  403. alysis_code/surface/types.py +100 -0
  404. alysis_code/swarm_backend.py +926 -0
  405. alysis_code/swarm_orchestrator.py +4020 -0
  406. alysis_code/swarm_scheduler.py +441 -0
  407. alysis_code/swarm_trace.py +429 -0
  408. alysis_code/swarm_worker.py +2119 -0
  409. alysis_code/swarm_write_guard.py +348 -0
  410. alysis_code/task_dependencies.py +170 -0
  411. alysis_code/task_readiness.py +992 -0
  412. alysis_code/task_scope.py +2148 -0
  413. alysis_code/terminal_manager.py +762 -0
  414. alysis_code/terminal_ownership.py +460 -0
  415. alysis_code/text_normalization.py +30 -0
  416. alysis_code/token_budget.py +97 -0
  417. alysis_code/tools/README.md +34 -0
  418. alysis_code/tools/__init__.py +1 -0
  419. alysis_code/tools/artifacts.py +127 -0
  420. alysis_code/tools/availability.py +188 -0
  421. alysis_code/tools/fs.py +1456 -0
  422. alysis_code/tools/git.py +461 -0
  423. alysis_code/tools/history.py +229 -0
  424. alysis_code/tools/http_timeout.py +78 -0
  425. alysis_code/tools/image_generation.py +552 -0
  426. alysis_code/tools/registry.py +2936 -0
  427. alysis_code/tools/repo_map.py +476 -0
  428. alysis_code/tools/search.py +563 -0
  429. alysis_code/tools/shell.py +135 -0
  430. alysis_code/tools/symbols.py +1350 -0
  431. alysis_code/tools/test_discovery.py +643 -0
  432. alysis_code/tools/web.py +482 -0
  433. alysis_code/tools/web_search.py +2012 -0
  434. alysis_code/tools/web_search_dashscope.py +557 -0
  435. alysis_code/tools/web_search_ddgs.py +221 -0
  436. alysis_code/tools/web_search_provider_adapters.py +1429 -0
  437. alysis_code/tools/web_search_tavily.py +194 -0
  438. alysis_code/updates.py +933 -0
  439. alysis_code/usage_tracker.py +1990 -0
  440. alysis_code/verification_command_analysis.py +1004 -0
  441. alysis_code/verification_contract.py +574 -0
  442. alysis_code/verification_failure_summary.py +273 -0
  443. alysis_code/verification_repair.py +385 -0
  444. alysis_code/verify_gate.py +3129 -0
  445. alysis_code/web_research.py +1872 -0
  446. alysis_code/web_search_adapters.py +66 -0
  447. alysis_code/web_search_policy.py +27 -0
  448. alysis_code/workspace_binding.py +389 -0
  449. alysis_code/workspace_binding_ui.py +408 -0
  450. alysis_code/workspace_context.py +273 -0
  451. alysis_code/workspace_isolation.py +138 -0
  452. alysis_code/workspace_provisioning.py +455 -0
  453. alysis_code-0.13.0.dist-info/METADATA +507 -0
  454. alysis_code-0.13.0.dist-info/RECORD +458 -0
  455. alysis_code-0.13.0.dist-info/WHEEL +4 -0
  456. alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
  457. alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
  458. alysis_code-0.13.0.dist-info/licenses/NOTICE +4 -0
@@ -0,0 +1,461 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import re
5
+ import subprocess
6
+ import tempfile
7
+ import threading
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from ..diff_paths import iter_patch_paths
12
+ from .fs import (
13
+ FilePrecondition,
14
+ FsError,
15
+ StaleFileError,
16
+ assert_file_precondition,
17
+ capture_file_precondition,
18
+ )
19
+
20
+
21
+ class GitError(RuntimeError):
22
+ pass
23
+
24
+
25
+ class GitStaleFileError(GitError, StaleFileError):
26
+ """A patch precondition conflict recognizable by both tool error contracts."""
27
+
28
+ def __init__(self, path: str) -> None:
29
+ StaleFileError.__init__(self, path)
30
+
31
+
32
+ _GIT_HISTORY_DEFAULT_LOG_LIMIT = 10
33
+ _GIT_HISTORY_MAX_LOG_LIMIT = 50
34
+ _GIT_HISTORY_LOG_BODY_MAX_CHARS = 240
35
+ _GIT_HISTORY_SHOW_BODY_MAX_CHARS = 1200
36
+ _GIT_HISTORY_SHOW_PATCH_MAX_CHARS = 6000
37
+ _GIT_HISTORY_BLAME_MAX_LINES = 200
38
+ _GIT_HISTORY_BLAME_LINE_MAX_CHARS = 240
39
+ _VALID_UNIFIED_HUNK_RE = re.compile(r"^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@(?: .*)?$")
40
+ _PATCH_LOCKS_GUARD = threading.Lock()
41
+ _PATCH_LOCKS: dict[str, threading.RLock] = {}
42
+
43
+
44
+ def _run_git(
45
+ root: Path,
46
+ args: list[str],
47
+ *,
48
+ input_s: str | None = None,
49
+ env: dict[str, str] | None = None,
50
+ ) -> subprocess.CompletedProcess[str]:
51
+ try:
52
+ return subprocess.run(
53
+ ["git", "-C", str(root), *args],
54
+ input=input_s,
55
+ check=False,
56
+ capture_output=True,
57
+ text=True,
58
+ env=env,
59
+ )
60
+ except OSError as e:
61
+ raise GitError("git not available") from e
62
+
63
+
64
+ def _normalize_git_error(stderr: str, *, fallback: str) -> str:
65
+ message = stderr.strip()
66
+ lowered = message.lower()
67
+ if "not a git repository" in lowered:
68
+ return "not a git repository"
69
+ if "unknown revision or path not in the working tree" in lowered:
70
+ return "invalid revision or path"
71
+ if "bad revision" in lowered or "bad object" in lowered:
72
+ return "invalid revision"
73
+ if "no such path" in lowered or "no such file" in lowered:
74
+ return "invalid path"
75
+ return message or fallback
76
+
77
+
78
+ def _run_git_checked(
79
+ *,
80
+ root: Path,
81
+ args: list[str],
82
+ input_s: str | None = None,
83
+ env: dict[str, str] | None = None,
84
+ fallback: str,
85
+ ) -> subprocess.CompletedProcess[str]:
86
+ cp = _run_git(root, args, input_s=input_s, env=env)
87
+ if cp.returncode != 0:
88
+ raise GitError(_normalize_git_error(cp.stderr, fallback=fallback))
89
+ return cp
90
+
91
+
92
+ def _normalize_patch_text(patch: str) -> str:
93
+ normalized = patch.replace("\r\n", "\n").replace("\r", "\n")
94
+ if normalized and not normalized.endswith("\n"):
95
+ normalized += "\n"
96
+ return normalized
97
+
98
+
99
+ def _validate_patch_shape(patch_text: str) -> None:
100
+ if not patch_text.strip():
101
+ raise GitError("malformed patch: patch is empty")
102
+
103
+ patch_paths = iter_patch_paths(patch_text)
104
+ if not patch_paths:
105
+ raise GitError("malformed patch: no file paths found in patch headers")
106
+
107
+ lines = patch_text.splitlines()
108
+ has_old_header = any(line.startswith("--- ") for line in lines)
109
+ has_new_header = any(line.startswith("+++ ") for line in lines)
110
+ if has_old_header != has_new_header:
111
+ raise GitError("malformed patch: missing paired ---/+++ file headers")
112
+
113
+ invalid_hunk_header = next(
114
+ (
115
+ line
116
+ for line in lines
117
+ if line.startswith("@@") and not _VALID_UNIFIED_HUNK_RE.match(line)
118
+ ),
119
+ None,
120
+ )
121
+ if invalid_hunk_header is None:
122
+ return
123
+ if invalid_hunk_header.startswith("@@ ..."):
124
+ raise GitError("malformed patch: placeholder hunk header '@@ ...' is not allowed")
125
+ raise GitError(f"malformed patch: invalid hunk header: {invalid_hunk_header}")
126
+
127
+
128
+ def _patch_lock(root: Path) -> threading.RLock:
129
+ key = os.path.normcase(os.fspath(root.resolve()))
130
+ with _PATCH_LOCKS_GUARD:
131
+ lock = _PATCH_LOCKS.get(key)
132
+ if lock is None:
133
+ lock = threading.RLock()
134
+ _PATCH_LOCKS[key] = lock
135
+ return lock
136
+
137
+
138
+ def _capture_patch_preconditions(root: Path, patch_text: str) -> tuple[FilePrecondition, ...]:
139
+ preconditions: list[FilePrecondition] = []
140
+ for raw_path in iter_patch_paths(patch_text):
141
+ normalized_path = _resolve_git_path(root, raw_path)
142
+ try:
143
+ preconditions.append(capture_file_precondition(root=root, path=normalized_path))
144
+ except StaleFileError as exc:
145
+ raise GitStaleFileError(exc.path) from exc
146
+ except FsError as exc:
147
+ raise GitError(f"Cannot safely prepare patch path {normalized_path}: {exc}") from exc
148
+ return tuple(preconditions)
149
+
150
+
151
+ def _assert_patch_preconditions(
152
+ root: Path,
153
+ preconditions: tuple[FilePrecondition, ...],
154
+ ) -> None:
155
+ for precondition in preconditions:
156
+ try:
157
+ assert_file_precondition(root=root, precondition=precondition)
158
+ except StaleFileError as exc:
159
+ raise GitStaleFileError(exc.path) from exc
160
+
161
+
162
+ def _resolve_git_path(root: Path, user_path: str) -> str:
163
+ root_abs = root.resolve()
164
+ target = (root_abs / user_path).resolve()
165
+ try:
166
+ rel = target.relative_to(root_abs)
167
+ except ValueError as e:
168
+ raise GitError(f"Path escapes root: {user_path}") from e
169
+ return os.fspath(rel).replace("\\", "/")
170
+
171
+
172
+ def _clip_text(text: str, *, max_chars: int) -> tuple[str, bool]:
173
+ normalized = text.strip()
174
+ if len(normalized) <= max_chars:
175
+ return normalized, False
176
+ return normalized[:max_chars].rstrip() + "...(truncated)", True
177
+
178
+
179
+ def _parse_log_records(stdout: str) -> list[dict[str, str]]:
180
+ commits: list[dict[str, str]] = []
181
+ for raw_record in stdout.split("\x1e"):
182
+ record = raw_record.strip("\x1e\r\n")
183
+ if not record:
184
+ continue
185
+ fields = record.split("\x1f", 6)
186
+ if len(fields) != 7:
187
+ continue
188
+ commit, short_commit, author_name, author_email, authored_at, subject, body = fields
189
+ body_excerpt, body_truncated = _clip_text(body, max_chars=_GIT_HISTORY_LOG_BODY_MAX_CHARS)
190
+ commits.append(
191
+ {
192
+ "commit": commit,
193
+ "short_commit": short_commit,
194
+ "author_name": author_name,
195
+ "author_email": author_email,
196
+ "authored_at": authored_at,
197
+ "subject": subject,
198
+ "body_excerpt": body_excerpt,
199
+ "body_truncated": body_truncated,
200
+ }
201
+ )
202
+ return commits
203
+
204
+
205
+ def git_status(*, root: Path) -> dict[str, Any]:
206
+ cp = _run_git_checked(
207
+ root=root,
208
+ args=["status", "--porcelain=v1", "-b"],
209
+ fallback="git status failed",
210
+ )
211
+ return {"status": cp.stdout}
212
+
213
+
214
+ def git_diff(*, root: Path) -> dict[str, Any]:
215
+ cp = _run_git_checked(root=root, args=["diff"], fallback="git diff failed")
216
+ diff = cp.stdout
217
+ if len(diff) > 20000:
218
+ diff = diff[:20000] + "...(truncated)"
219
+ return {"diff": diff}
220
+
221
+
222
+ def git_history(
223
+ *,
224
+ root: Path,
225
+ mode: str,
226
+ path: str | None = None,
227
+ limit: int = _GIT_HISTORY_DEFAULT_LOG_LIMIT,
228
+ ref: str | None = None,
229
+ grep: str | None = None,
230
+ author: str | None = None,
231
+ commit: str | None = None,
232
+ start_line: int | None = None,
233
+ end_line: int | None = None,
234
+ ) -> dict[str, Any]:
235
+ normalized_mode = str(mode or "").strip().lower()
236
+ if normalized_mode not in {"log", "show", "blame"}:
237
+ raise GitError(f"Unsupported git_history mode: {mode!r}")
238
+
239
+ normalized_path = None
240
+ if path is not None and str(path).strip():
241
+ normalized_path = _resolve_git_path(root, str(path))
242
+
243
+ if normalized_mode == "log":
244
+ safe_limit = max(1, min(int(limit), _GIT_HISTORY_MAX_LOG_LIMIT))
245
+ ref_value = str(ref or "HEAD").strip() or "HEAD"
246
+ args = [
247
+ "log",
248
+ f"-n{safe_limit + 1}",
249
+ "--date=iso-strict",
250
+ "--format=format:%H%x1f%h%x1f%an%x1f%ae%x1f%aI%x1f%s%x1f%b%x1e",
251
+ ]
252
+ if grep:
253
+ args.extend(["--grep", str(grep)])
254
+ if author:
255
+ args.extend(["--author", str(author)])
256
+ args.append(ref_value)
257
+ if normalized_path is not None:
258
+ args.extend(["--", normalized_path])
259
+ cp = _run_git_checked(root=root, args=args, fallback="git log failed")
260
+ commits = _parse_log_records(cp.stdout)
261
+ truncated = len(commits) > safe_limit
262
+ return {
263
+ "mode": "log",
264
+ "path": normalized_path,
265
+ "limit": safe_limit,
266
+ "ref": ref_value,
267
+ "grep": str(grep) if grep else None,
268
+ "author": str(author) if author else None,
269
+ "commits": commits[:safe_limit],
270
+ "truncated": truncated,
271
+ }
272
+
273
+ if normalized_mode == "show":
274
+ explicit_commit = str(commit or "").strip()
275
+ explicit_ref = str(ref or "").strip()
276
+ if explicit_commit and explicit_ref and explicit_commit != explicit_ref:
277
+ raise GitError("show mode commit and ref must name the same revision")
278
+ commit_value = explicit_commit or explicit_ref
279
+ if not commit_value:
280
+ raise GitError("show mode requires commit or ref")
281
+ meta_cp = _run_git_checked(
282
+ root=root,
283
+ args=[
284
+ "show",
285
+ "--quiet",
286
+ "--no-patch",
287
+ "--date=iso-strict",
288
+ "--format=format:%H%x1f%h%x1f%an%x1f%ae%x1f%aI%x1f%s%x1f%b",
289
+ commit_value,
290
+ ],
291
+ fallback="git show failed",
292
+ )
293
+ fields = meta_cp.stdout.split("\x1f", 6)
294
+ if len(fields) != 7:
295
+ raise GitError("git show returned an unexpected format")
296
+ body_excerpt, body_truncated = _clip_text(
297
+ fields[6], max_chars=_GIT_HISTORY_SHOW_BODY_MAX_CHARS
298
+ )
299
+
300
+ patch_args = ["show", "--format=", "--patch", "--no-ext-diff", "--no-color", commit_value]
301
+ if normalized_path is not None:
302
+ patch_args.extend(["--", normalized_path])
303
+ patch_cp = _run_git_checked(root=root, args=patch_args, fallback="git show failed")
304
+ patch_excerpt, patch_truncated = _clip_text(
305
+ patch_cp.stdout, max_chars=_GIT_HISTORY_SHOW_PATCH_MAX_CHARS
306
+ )
307
+
308
+ return {
309
+ "mode": "show",
310
+ "path": normalized_path,
311
+ "commit": {
312
+ "commit": fields[0],
313
+ "short_commit": fields[1],
314
+ "author_name": fields[2],
315
+ "author_email": fields[3],
316
+ "authored_at": fields[4],
317
+ "subject": fields[5],
318
+ "body_excerpt": body_excerpt,
319
+ "body_truncated": body_truncated,
320
+ },
321
+ "patch_excerpt": patch_excerpt,
322
+ "patch_truncated": patch_truncated,
323
+ }
324
+
325
+ if normalized_path is None:
326
+ raise GitError("blame mode requires path")
327
+ if start_line is None or end_line is None:
328
+ raise GitError("blame mode requires start_line and end_line")
329
+ if start_line < 1:
330
+ raise GitError(f"Invalid start_line: {start_line} (must be >= 1)")
331
+ if end_line < start_line:
332
+ raise GitError(
333
+ f"Invalid line range: end_line ({end_line}) must be >= start_line ({start_line})"
334
+ )
335
+ requested_lines = end_line - start_line + 1
336
+ if requested_lines > _GIT_HISTORY_BLAME_MAX_LINES:
337
+ raise GitError(
338
+ "Requested blame range too large: "
339
+ f"{requested_lines} lines (max {_GIT_HISTORY_BLAME_MAX_LINES})"
340
+ )
341
+
342
+ cp = _run_git_checked(
343
+ root=root,
344
+ args=[
345
+ "blame",
346
+ "--line-porcelain",
347
+ f"-L{start_line},{end_line}",
348
+ "--",
349
+ normalized_path,
350
+ ],
351
+ fallback="git blame failed",
352
+ )
353
+
354
+ lines: list[dict[str, Any]] = []
355
+ current: dict[str, Any] | None = None
356
+ for raw_line in cp.stdout.splitlines():
357
+ if raw_line.startswith("\t"):
358
+ if current is None:
359
+ continue
360
+ line_text, line_truncated = _clip_text(
361
+ raw_line[1:], max_chars=_GIT_HISTORY_BLAME_LINE_MAX_CHARS
362
+ )
363
+ lines.append(
364
+ {
365
+ "line_number": int(current["line_number"]),
366
+ "commit": str(current["commit"]),
367
+ "short_commit": str(current["commit"])[:12],
368
+ "author_name": str(current.get("author") or ""),
369
+ "author_email": str(current.get("author-mail") or "").strip("<>"),
370
+ "summary": str(current.get("summary") or ""),
371
+ "content": line_text,
372
+ "content_truncated": line_truncated,
373
+ }
374
+ )
375
+ current = None
376
+ continue
377
+
378
+ parts = raw_line.split()
379
+ if len(parts) >= 4 and len(parts[0]) >= 8:
380
+ try:
381
+ final_lineno = int(parts[2])
382
+ except ValueError:
383
+ final_lineno = 0
384
+ current = {"commit": parts[0], "line_number": final_lineno}
385
+ continue
386
+
387
+ if current is None:
388
+ continue
389
+ key, _, value = raw_line.partition(" ")
390
+ current[key] = value
391
+
392
+ actual_end_line = end_line if not lines else int(lines[-1]["line_number"])
393
+ return {
394
+ "mode": "blame",
395
+ "path": normalized_path,
396
+ "start_line": start_line,
397
+ "end_line": actual_end_line,
398
+ "lines": lines,
399
+ "truncated": False,
400
+ }
401
+
402
+
403
+ def git_apply_patch(*, root: Path, patch: str) -> dict[str, Any]:
404
+ normalized_patch = _normalize_patch_text(patch)
405
+ _validate_patch_shape(normalized_patch)
406
+ with _patch_lock(root):
407
+ preconditions = _capture_patch_preconditions(root, normalized_patch)
408
+ with tempfile.TemporaryDirectory(prefix="alysis-patch-index-") as temporary:
409
+ patch_env = dict(os.environ)
410
+ patch_env["GIT_INDEX_FILE"] = os.fspath(Path(temporary) / "index")
411
+ no_filters = ["-c", "core.autocrlf=false"]
412
+ _run_git_checked(
413
+ root=root,
414
+ args=[*no_filters, "read-tree", "--empty"],
415
+ env=patch_env,
416
+ fallback="git apply preflight failed",
417
+ )
418
+ existing_paths = [item.path for item in preconditions if item.exists]
419
+ if existing_paths:
420
+ _run_git_checked(
421
+ root=root,
422
+ args=[*no_filters, "add", "-f", "--", *existing_paths],
423
+ env=patch_env,
424
+ fallback="git apply preflight failed",
425
+ )
426
+ _assert_patch_preconditions(root, preconditions)
427
+ try:
428
+ _run_git_checked(
429
+ root=root,
430
+ args=[
431
+ *no_filters,
432
+ "apply",
433
+ "--check",
434
+ "--index",
435
+ "--whitespace=nowarn",
436
+ ],
437
+ input_s=normalized_patch,
438
+ env=patch_env,
439
+ fallback="git apply preflight failed",
440
+ )
441
+ except GitError as e:
442
+ raise GitError(f"git apply preflight failed: {e}") from e
443
+ _assert_patch_preconditions(root, preconditions)
444
+ try:
445
+ # The private index records the exact whole-file preconditions.
446
+ # ``--index`` rechecks those blobs inside the mutating Git process,
447
+ # without reading or changing the developer's real index.
448
+ _run_git_checked(
449
+ root=root,
450
+ args=[*no_filters, "apply", "--index", "--whitespace=nowarn"],
451
+ input_s=normalized_patch,
452
+ env=patch_env,
453
+ fallback="git apply failed",
454
+ )
455
+ except GitError as exc:
456
+ # If another writer won after our final user-space check, surface
457
+ # the deterministic stale-file contract rather than a generic
458
+ # index mismatch.
459
+ _assert_patch_preconditions(root, preconditions)
460
+ raise exc
461
+ return {"applied": True}
@@ -0,0 +1,229 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import re
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from ..ide.protocol import redact_secrets
10
+ from ..session_artifacts import SessionArtifactLayout
11
+
12
+
13
+ class HistorySearchError(RuntimeError):
14
+ pass
15
+
16
+
17
+ _SNIPPET_MAX_CHARS = 240
18
+ _MAX_RESULTS = 500
19
+ _MAX_FILE_BYTES = 1024 * 1024
20
+ _MAX_TOTAL_BYTES = 8 * 1024 * 1024
21
+ _MAX_TARGET_FILES = 2_000
22
+ _MAX_SNIPPET_CHARS = 4_000
23
+
24
+
25
+ def _safe_component(raw: str) -> str:
26
+ clean = re.sub(r"[^A-Za-z0-9_-]", "_", str(raw).strip())
27
+ return clean or "x"
28
+
29
+
30
+ def _clip_text(text: str, limit: int = _SNIPPET_MAX_CHARS) -> str:
31
+ compact = re.sub(r"\s+", " ", text.strip())
32
+ if len(compact) <= limit:
33
+ return compact
34
+ return compact[:limit].rstrip() + "..."
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class _ArtifactSearchRoot:
39
+ path: Path
40
+ layout: SessionArtifactLayout | None = None
41
+
42
+
43
+ def _search_roots(
44
+ *,
45
+ root: Path,
46
+ session_id: str,
47
+ session_artifact_root: Path | None,
48
+ ) -> list[_ArtifactSearchRoot]:
49
+ roots: list[_ArtifactSearchRoot] = []
50
+ seen: set[Path] = set()
51
+
52
+ if session_artifact_root is not None:
53
+ artifact_root = session_artifact_root.resolve()
54
+ if artifact_root.name == _safe_component(session_id) and artifact_root not in seen:
55
+ roots.append(
56
+ _ArtifactSearchRoot(
57
+ path=artifact_root,
58
+ layout=SessionArtifactLayout(filesystem_root=artifact_root),
59
+ )
60
+ )
61
+ seen.add(artifact_root)
62
+
63
+ history_root = (root / ".alysis" / "sessions" / _safe_component(session_id)).resolve()
64
+ if history_root not in seen:
65
+ roots.append(_ArtifactSearchRoot(path=history_root))
66
+ seen.add(history_root)
67
+ return roots
68
+
69
+
70
+ def _iter_target_files(
71
+ *,
72
+ roots: list[_ArtifactSearchRoot],
73
+ include_history: bool,
74
+ include_tool_outputs: bool,
75
+ include_memory: bool,
76
+ ) -> tuple[list[tuple[str, Path, SessionArtifactLayout | None]], bool]:
77
+ targets: list[tuple[str, Path, SessionArtifactLayout | None]] = []
78
+ for artifact_root in roots:
79
+ base = artifact_root.path
80
+ if include_history:
81
+ for path in sorted((base / "history").glob("chunk_*.jsonl")):
82
+ if _is_safe_artifact_file(path=path, root=base):
83
+ targets.append(("history", path, artifact_root.layout))
84
+ if len(targets) >= _MAX_TARGET_FILES:
85
+ return targets, True
86
+ if include_tool_outputs:
87
+ for path in sorted((base / "tool_outputs").glob("*.json")):
88
+ if _is_safe_artifact_file(path=path, root=base):
89
+ targets.append(("tool_output", path, artifact_root.layout))
90
+ if len(targets) >= _MAX_TARGET_FILES:
91
+ return targets, True
92
+ if include_memory:
93
+ for filename in ("summary.json", "pins.json"):
94
+ path = base / "memory" / filename
95
+ if _is_safe_artifact_file(path=path, root=base):
96
+ targets.append(("memory", path, artifact_root.layout))
97
+ if len(targets) >= _MAX_TARGET_FILES:
98
+ return targets, True
99
+ return targets, False
100
+
101
+
102
+ def _is_safe_artifact_file(*, path: Path, root: Path) -> bool:
103
+ try:
104
+ if path.is_symlink() or not path.is_file():
105
+ return False
106
+ path.resolve().relative_to(root.resolve())
107
+ except (OSError, RuntimeError, ValueError):
108
+ return False
109
+ return True
110
+
111
+
112
+ def _display_artifact_path(
113
+ *,
114
+ file_path: Path,
115
+ workspace_root: Path,
116
+ layout: SessionArtifactLayout | None,
117
+ ) -> str:
118
+ if layout is not None:
119
+ return layout.display_reference_for_path(
120
+ artifact_path=file_path,
121
+ workspace_root=workspace_root,
122
+ )
123
+ try:
124
+ return os.fspath(file_path.resolve().relative_to(workspace_root)).replace("\\", "/")
125
+ except ValueError:
126
+ return file_path.name
127
+
128
+
129
+ def history_search(
130
+ *,
131
+ root: Path,
132
+ session_id: str,
133
+ session_artifact_root: Path | None = None,
134
+ pattern: str,
135
+ max_results: int = 50,
136
+ max_file_bytes: int = 200_000,
137
+ max_total_bytes: int = _MAX_TOTAL_BYTES,
138
+ max_snippet_chars: int = _SNIPPET_MAX_CHARS,
139
+ include_history: bool = True,
140
+ include_tool_outputs: bool = True,
141
+ include_memory: bool = True,
142
+ ) -> dict[str, Any]:
143
+ root_abs = root.resolve()
144
+
145
+ max_results = max(1, min(int(max_results), _MAX_RESULTS))
146
+ max_file_bytes = max(1, min(int(max_file_bytes), _MAX_FILE_BYTES))
147
+ max_total_bytes = max(1, min(int(max_total_bytes), _MAX_TOTAL_BYTES))
148
+ max_snippet_chars = max(1, min(int(max_snippet_chars), _MAX_SNIPPET_CHARS))
149
+
150
+ try:
151
+ compiled = re.compile(pattern)
152
+ except re.error as exc:
153
+ raise HistorySearchError(f"Invalid regex pattern: {exc}") from exc
154
+
155
+ matches: list[dict[str, Any]] = []
156
+ truncated = False
157
+ result_limit_reached = False
158
+ scanned_bytes = 0
159
+ scanned_files = 0
160
+
161
+ search_roots = _search_roots(
162
+ root=root_abs,
163
+ session_id=session_id,
164
+ session_artifact_root=session_artifact_root,
165
+ )
166
+ if not any(search_root.path.exists() for search_root in search_roots):
167
+ return {
168
+ "pattern": str(redact_secrets(pattern)),
169
+ "matches": matches,
170
+ "truncated": truncated,
171
+ "scanned_bytes": scanned_bytes,
172
+ "scanned_files": scanned_files,
173
+ }
174
+
175
+ targets, target_list_truncated = _iter_target_files(
176
+ roots=search_roots,
177
+ include_history=include_history,
178
+ include_tool_outputs=include_tool_outputs,
179
+ include_memory=include_memory,
180
+ )
181
+ truncated = target_list_truncated
182
+
183
+ for kind, file_path, layout in targets:
184
+ remaining_bytes = max_total_bytes - scanned_bytes
185
+ if remaining_bytes <= 0:
186
+ truncated = True
187
+ break
188
+ read_limit = min(max_file_bytes, remaining_bytes)
189
+ try:
190
+ with file_path.open("rb") as fh:
191
+ data = fh.read(read_limit + 1)
192
+ except OSError:
193
+ continue
194
+ scanned_files += 1
195
+ if len(data) > read_limit:
196
+ data = data[:read_limit]
197
+ truncated = True
198
+ scanned_bytes += len(data)
199
+ text = data.decode("utf-8", errors="replace")
200
+
201
+ for line_no, line in enumerate(text.splitlines(), start=1):
202
+ if not compiled.search(line):
203
+ continue
204
+ matches.append(
205
+ {
206
+ "kind": kind,
207
+ "path": _display_artifact_path(
208
+ file_path=file_path,
209
+ workspace_root=root_abs,
210
+ layout=layout,
211
+ ),
212
+ "line": line_no,
213
+ "text": str(redact_secrets(_clip_text(line, max_snippet_chars))),
214
+ }
215
+ )
216
+ if len(matches) >= max_results:
217
+ truncated = True
218
+ result_limit_reached = True
219
+ break
220
+ if result_limit_reached:
221
+ break
222
+
223
+ return {
224
+ "pattern": str(redact_secrets(pattern)),
225
+ "matches": matches,
226
+ "truncated": truncated,
227
+ "scanned_bytes": scanned_bytes,
228
+ "scanned_files": scanned_files,
229
+ }