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,227 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ from ..runtime_kind import RuntimeKind, normalize_runtime_kind
7
+ from .discovery import (
8
+ CustomToolCapabilities,
9
+ CustomToolDiscoveryResult,
10
+ CustomToolIssue,
11
+ CustomToolSpec,
12
+ discover_custom_tools,
13
+ )
14
+ from .trust import ProjectToolTrustState, is_project_tool_trusted, load_trust_state
15
+
16
+ _CUSTOM_TOOL_TOP_LEVEL_RUNTIMES = {
17
+ RuntimeKind.INTERACTIVE_CHAT,
18
+ RuntimeKind.ONE_SHOT,
19
+ RuntimeKind.FORGE_EXEC,
20
+ }
21
+ _CUSTOM_TOOL_TOP_LEVEL_RUNTIME_VALUES = frozenset(
22
+ runtime_kind.value for runtime_kind in _CUSTOM_TOOL_TOP_LEVEL_RUNTIMES
23
+ )
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class CustomToolCatalogEntry:
28
+ name: str
29
+ source_scope: str
30
+ trust: str
31
+ status: str
32
+ source_path: Path
33
+ detail: str = ""
34
+ manifest_version: int | None = None
35
+ capabilities: CustomToolCapabilities | None = None
36
+ spec: CustomToolSpec | None = None
37
+ issue: CustomToolIssue | None = None
38
+ exposed: bool = False
39
+
40
+ @property
41
+ def normalized_name(self) -> str:
42
+ return self.name.casefold()
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class CustomToolSessionState:
47
+ discovery: CustomToolDiscoveryResult
48
+ trust_state: ProjectToolTrustState
49
+ catalog_entries: tuple[CustomToolCatalogEntry, ...]
50
+ effective_tools_by_name: dict[str, CustomToolSpec]
51
+ exposed_tools_by_name: dict[str, CustomToolSpec]
52
+
53
+
54
+ def build_custom_tool_session_state(
55
+ *,
56
+ workspace_root: Path,
57
+ custom_tools_enabled: bool,
58
+ mode: str,
59
+ runtime_kind: RuntimeKind | str,
60
+ built_in_tool_names: set[str],
61
+ catalog_view: bool = False,
62
+ write_scope_restricted: bool = False,
63
+ user_config_dir: Path | None = None,
64
+ discovery: CustomToolDiscoveryResult | None = None,
65
+ trust_state: ProjectToolTrustState | None = None,
66
+ ) -> CustomToolSessionState:
67
+ resolved_runtime_kind = normalize_runtime_kind(
68
+ runtime_kind, fallback=RuntimeKind.INTERACTIVE_CHAT
69
+ )
70
+ active_discovery = discovery or discover_custom_tools(
71
+ workspace_root=workspace_root,
72
+ built_in_tool_names=built_in_tool_names,
73
+ user_config_dir=user_config_dir,
74
+ )
75
+ active_trust_state = trust_state
76
+ if active_trust_state is None:
77
+ try:
78
+ active_trust_state = load_trust_state(user_config_dir=user_config_dir)
79
+ except RuntimeError:
80
+ active_trust_state = ProjectToolTrustState()
81
+
82
+ effective = active_discovery.effective_tools_by_name()
83
+ exposed: dict[str, CustomToolSpec] = {}
84
+ entries: list[CustomToolCatalogEntry] = []
85
+ shadowed_names = {tool.normalized_name for tool in active_discovery.shadowed_tools}
86
+
87
+ for tool in active_discovery.effective_tools:
88
+ status, trust_label, detail = _tool_status(
89
+ tool=tool,
90
+ custom_tools_enabled=custom_tools_enabled,
91
+ mode=mode,
92
+ runtime_kind=resolved_runtime_kind,
93
+ catalog_view=catalog_view,
94
+ write_scope_restricted=write_scope_restricted,
95
+ trust_state=active_trust_state,
96
+ )
97
+ exposed_flag = status == "available" and not catalog_view
98
+ if exposed_flag:
99
+ exposed[tool.normalized_name] = tool
100
+ entries.append(
101
+ CustomToolCatalogEntry(
102
+ name=tool.name,
103
+ source_scope=tool.source_scope,
104
+ trust=trust_label,
105
+ status=status,
106
+ source_path=tool.source_path,
107
+ detail=detail,
108
+ manifest_version=tool.manifest_version,
109
+ capabilities=tool.capabilities,
110
+ spec=tool,
111
+ exposed=exposed_flag,
112
+ )
113
+ )
114
+
115
+ for tool in active_discovery.shadowed_tools:
116
+ entries.append(
117
+ CustomToolCatalogEntry(
118
+ name=tool.name,
119
+ source_scope=tool.source_scope,
120
+ trust="global" if tool.source_scope == "global" else "persistent",
121
+ status="shadowed",
122
+ source_path=tool.source_path,
123
+ detail="overridden by a project custom tool with the same name",
124
+ manifest_version=tool.manifest_version,
125
+ capabilities=tool.capabilities,
126
+ spec=tool,
127
+ )
128
+ )
129
+
130
+ for issue in active_discovery.issues:
131
+ issue_name = issue.tool_name.strip() or Path(issue.relative_tool_path).stem
132
+ if issue_name.casefold() in shadowed_names:
133
+ detail = issue.message
134
+ else:
135
+ detail = issue.message
136
+ entries.append(
137
+ CustomToolCatalogEntry(
138
+ name=issue_name,
139
+ source_scope=issue.source_scope,
140
+ trust="-",
141
+ status="invalid",
142
+ source_path=issue.source_path,
143
+ detail=detail,
144
+ issue=issue,
145
+ )
146
+ )
147
+
148
+ entries_sorted = tuple(
149
+ sorted(
150
+ entries,
151
+ key=lambda entry: (
152
+ entry.name.casefold(),
153
+ 0 if entry.status == "available" else 1,
154
+ 0 if entry.source_scope == "project" else 1,
155
+ entry.source_path.as_posix().casefold(),
156
+ ),
157
+ )
158
+ )
159
+ return CustomToolSessionState(
160
+ discovery=active_discovery,
161
+ trust_state=active_trust_state,
162
+ catalog_entries=entries_sorted,
163
+ effective_tools_by_name=effective,
164
+ exposed_tools_by_name=exposed,
165
+ )
166
+
167
+
168
+ def _tool_status(
169
+ *,
170
+ tool: CustomToolSpec,
171
+ custom_tools_enabled: bool,
172
+ mode: str,
173
+ runtime_kind: RuntimeKind,
174
+ catalog_view: bool,
175
+ write_scope_restricted: bool,
176
+ trust_state: ProjectToolTrustState,
177
+ ) -> tuple[str, str, str]:
178
+ if tool.source_scope == "global":
179
+ trust_label = "global"
180
+ trusted = True
181
+ else:
182
+ trusted = is_project_tool_trusted(tool, state=trust_state)
183
+ trust_label = "persistent" if trusted else "untrusted"
184
+
185
+ if not custom_tools_enabled:
186
+ return "disabled", trust_label, "custom tools are disabled in config"
187
+ if catalog_view:
188
+ if not _tool_enabled_in_any_supported_top_level_runtime(tool):
189
+ return (
190
+ "disabled",
191
+ trust_label,
192
+ "tool manifest does not enable any supported top-level runtime",
193
+ )
194
+ if tool.missing_env:
195
+ missing = ", ".join(tool.missing_env)
196
+ return "missing-env", trust_label, f"missing required env vars: {missing}"
197
+ if not trusted:
198
+ return "untrusted", trust_label, "project tool requires persistent trust"
199
+ return "available", trust_label, ""
200
+ if write_scope_restricted:
201
+ return (
202
+ "disabled",
203
+ trust_label,
204
+ "custom tools are unavailable when write scope is narrowed",
205
+ )
206
+ if not _custom_tools_supported_runtime(mode=mode, runtime_kind=runtime_kind):
207
+ return "disabled", trust_label, "custom tools are unavailable in this runtime or mode"
208
+ if runtime_kind.value not in set(tool.enabled_in):
209
+ return "disabled", trust_label, "tool manifest disables this runtime"
210
+ if tool.missing_env:
211
+ missing = ", ".join(tool.missing_env)
212
+ return "missing-env", trust_label, f"missing required env vars: {missing}"
213
+ if not trusted:
214
+ return "untrusted", trust_label, "project tool requires persistent trust"
215
+ return "available", trust_label, ""
216
+
217
+
218
+ def _custom_tools_supported_runtime(*, mode: str, runtime_kind: RuntimeKind) -> bool:
219
+ if str(mode or "").strip().lower() == "readonly":
220
+ return False
221
+ return runtime_kind in _CUSTOM_TOOL_TOP_LEVEL_RUNTIMES
222
+
223
+
224
+ def _tool_enabled_in_any_supported_top_level_runtime(tool: CustomToolSpec) -> bool:
225
+ return any(
226
+ runtime_kind in _CUSTOM_TOOL_TOP_LEVEL_RUNTIME_VALUES for runtime_kind in tool.enabled_in
227
+ )
@@ -0,0 +1,232 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import threading
6
+ from collections.abc import Callable
7
+ from contextlib import contextmanager
8
+ from dataclasses import dataclass
9
+ from io import BufferedRandom
10
+ from pathlib import Path
11
+
12
+ from ..atomic_io import atomic_write_json
13
+ from ..branding import canonical_user_config_dir, env_get
14
+ from .discovery import CustomToolSpec
15
+
16
+ _TRUST_SCHEMA_VERSION = 1
17
+ _TRUST_STATE_LOCK = threading.RLock()
18
+
19
+ if os.name == "nt": # pragma: no cover - exercised on Windows
20
+ import msvcrt
21
+ else: # pragma: no cover - exercised on POSIX
22
+ import fcntl
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class ProjectToolTrustKey:
27
+ workspace_root: str
28
+ relative_tool_path: str
29
+ file_hash: str
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class ProjectToolTrustState:
34
+ trusted_tools: tuple[ProjectToolTrustKey, ...] = ()
35
+
36
+ def contains(self, key: ProjectToolTrustKey) -> bool:
37
+ return key in set(self.trusted_tools)
38
+
39
+
40
+ def _config_dir() -> Path:
41
+ override = env_get("ALYSIS_CONFIG_DIR")
42
+ if override:
43
+ return Path(override)
44
+ return canonical_user_config_dir()
45
+
46
+
47
+ def trust_state_path(*, user_config_dir: Path | None = None) -> Path:
48
+ base = user_config_dir.expanduser().resolve() if user_config_dir is not None else _config_dir()
49
+ return base / "custom_tools_trust.json"
50
+
51
+
52
+ def load_trust_state(*, user_config_dir: Path | None = None) -> ProjectToolTrustState:
53
+ path = trust_state_path(user_config_dir=user_config_dir)
54
+ return _load_trust_state_from_path(path)
55
+
56
+
57
+ def _load_trust_state_from_path(path: Path) -> ProjectToolTrustState:
58
+ if not path.exists():
59
+ return ProjectToolTrustState()
60
+ try:
61
+ raw = json.loads(path.read_text(encoding="utf-8"))
62
+ except Exception as exc: # noqa: BLE001
63
+ raise RuntimeError(f"Failed to read custom tool trust state: {path}") from exc
64
+ if not isinstance(raw, dict):
65
+ raise RuntimeError("Invalid custom tool trust state format.")
66
+ version = raw.get("schema_version", _TRUST_SCHEMA_VERSION)
67
+ if version != _TRUST_SCHEMA_VERSION:
68
+ raise RuntimeError(f"Unsupported custom tool trust schema version: {version}")
69
+ trusted_raw = raw.get("trusted_tools", [])
70
+ if not isinstance(trusted_raw, list):
71
+ raise RuntimeError("Invalid custom tool trust state: trusted_tools must be an array.")
72
+ trusted: list[ProjectToolTrustKey] = []
73
+ for item in trusted_raw:
74
+ if not isinstance(item, dict):
75
+ continue
76
+ workspace_root = str(item.get("workspace_root") or "").strip()
77
+ relative_tool_path = str(item.get("relative_tool_path") or "").strip()
78
+ file_hash = str(item.get("file_hash") or "").strip()
79
+ if workspace_root and relative_tool_path and file_hash:
80
+ trusted.append(
81
+ ProjectToolTrustKey(
82
+ workspace_root=workspace_root,
83
+ relative_tool_path=relative_tool_path,
84
+ file_hash=file_hash,
85
+ )
86
+ )
87
+ trusted_sorted = tuple(
88
+ sorted(
89
+ set(trusted),
90
+ key=lambda key: (key.workspace_root, key.relative_tool_path, key.file_hash),
91
+ )
92
+ )
93
+ return ProjectToolTrustState(trusted_tools=trusted_sorted)
94
+
95
+
96
+ def save_trust_state(
97
+ state: ProjectToolTrustState,
98
+ *,
99
+ user_config_dir: Path | None = None,
100
+ ) -> None:
101
+ path = trust_state_path(user_config_dir=user_config_dir)
102
+ with _locked_trust_state(path):
103
+ _save_trust_state_to_path(state, path)
104
+
105
+
106
+ def _save_trust_state_to_path(
107
+ state: ProjectToolTrustState,
108
+ path: Path,
109
+ ) -> None:
110
+ path.parent.mkdir(parents=True, exist_ok=True)
111
+ payload = {
112
+ "schema_version": _TRUST_SCHEMA_VERSION,
113
+ "trusted_tools": [
114
+ {
115
+ "workspace_root": key.workspace_root,
116
+ "relative_tool_path": key.relative_tool_path,
117
+ "file_hash": key.file_hash,
118
+ }
119
+ for key in state.trusted_tools
120
+ ],
121
+ }
122
+ atomic_write_json(path, payload, ensure_ascii=True)
123
+
124
+
125
+ def trust_project_tool(
126
+ spec: CustomToolSpec,
127
+ *,
128
+ user_config_dir: Path | None = None,
129
+ ) -> ProjectToolTrustState:
130
+ key = project_tool_trust_key(spec)
131
+ return _mutate_trust_state(
132
+ user_config_dir=user_config_dir,
133
+ mutation=lambda current: ProjectToolTrustState(
134
+ trusted_tools=tuple(
135
+ sorted(
136
+ {*(current.trusted_tools), key},
137
+ key=lambda item: (item.workspace_root, item.relative_tool_path, item.file_hash),
138
+ )
139
+ )
140
+ ),
141
+ )
142
+
143
+
144
+ def untrust_project_tool(
145
+ spec: CustomToolSpec,
146
+ *,
147
+ user_config_dir: Path | None = None,
148
+ ) -> ProjectToolTrustState:
149
+ if spec.source_scope != "project":
150
+ raise ValueError("Only project custom tools can be untrusted.")
151
+ workspace_root = os.fspath(spec.source_path.resolve().parents[2])
152
+ relpath = spec.relative_tool_path
153
+ return _mutate_trust_state(
154
+ user_config_dir=user_config_dir,
155
+ mutation=lambda current: ProjectToolTrustState(
156
+ trusted_tools=tuple(
157
+ key
158
+ for key in current.trusted_tools
159
+ if not (key.workspace_root == workspace_root and key.relative_tool_path == relpath)
160
+ )
161
+ ),
162
+ )
163
+
164
+
165
+ def is_project_tool_trusted(
166
+ spec: CustomToolSpec,
167
+ *,
168
+ state: ProjectToolTrustState | None = None,
169
+ user_config_dir: Path | None = None,
170
+ ) -> bool:
171
+ if spec.source_scope != "project":
172
+ return True
173
+ active_state = state if state is not None else load_trust_state(user_config_dir=user_config_dir)
174
+ return project_tool_trust_key(spec) in set(active_state.trusted_tools)
175
+
176
+
177
+ def project_tool_trust_key(spec: CustomToolSpec) -> ProjectToolTrustKey:
178
+ if spec.source_scope != "project":
179
+ raise ValueError("Only project custom tools use explicit trust keys.")
180
+ workspace_root = os.fspath(spec.source_path.resolve().parents[2])
181
+ return ProjectToolTrustKey(
182
+ workspace_root=workspace_root,
183
+ relative_tool_path=spec.relative_tool_path,
184
+ file_hash=spec.file_hash,
185
+ )
186
+
187
+
188
+ def _mutate_trust_state(
189
+ *,
190
+ user_config_dir: Path | None,
191
+ mutation: Callable[[ProjectToolTrustState], ProjectToolTrustState],
192
+ ) -> ProjectToolTrustState:
193
+ path = trust_state_path(user_config_dir=user_config_dir)
194
+ with _locked_trust_state(path):
195
+ current = _load_trust_state_from_path(path)
196
+ updated = mutation(current)
197
+ if updated != current:
198
+ _save_trust_state_to_path(updated, path)
199
+ return updated
200
+
201
+
202
+ @contextmanager
203
+ def _locked_trust_state(path: Path):
204
+ path.parent.mkdir(parents=True, exist_ok=True)
205
+ lock_path = path.with_name(f"{path.name}.lock")
206
+ with _TRUST_STATE_LOCK:
207
+ with lock_path.open("a+b") as handle:
208
+ _acquire_file_lock(handle)
209
+ try:
210
+ yield
211
+ finally:
212
+ _release_file_lock(handle)
213
+
214
+
215
+ def _acquire_file_lock(handle: BufferedRandom) -> None:
216
+ if os.name == "nt": # pragma: no cover - exercised on Windows
217
+ handle.seek(0, os.SEEK_END)
218
+ if handle.tell() == 0:
219
+ handle.write(b"\0")
220
+ handle.flush()
221
+ handle.seek(0)
222
+ msvcrt.locking(handle.fileno(), msvcrt.LK_LOCK, 1)
223
+ return
224
+ fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
225
+
226
+
227
+ def _release_file_lock(handle: BufferedRandom) -> None:
228
+ if os.name == "nt": # pragma: no cover - exercised on Windows
229
+ handle.seek(0)
230
+ msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
231
+ return
232
+ fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ import shlex
4
+
5
+
6
+ def _unquote_path(value: str) -> str:
7
+ raw = value.strip()
8
+ if not raw:
9
+ return ""
10
+ if raw[0] == raw[-1] == '"' and len(raw) >= 2:
11
+ try:
12
+ parsed = shlex.split(raw)
13
+ if parsed:
14
+ return parsed[0]
15
+ except ValueError:
16
+ return raw[1:-1]
17
+ return raw[1:-1]
18
+ return raw
19
+
20
+
21
+ def _normalize_git_path(value: str) -> str:
22
+ raw = _unquote_path(value.strip())
23
+ if raw.startswith("a/") or raw.startswith("b/"):
24
+ return raw[2:]
25
+ return raw
26
+
27
+
28
+ def parse_diff_git_line(line: str) -> tuple[str | None, str | None]:
29
+ if not line.startswith("diff --git "):
30
+ return None, None
31
+ payload = line[len("diff --git ") :].strip()
32
+
33
+ if payload.startswith("a/"):
34
+ split = payload.rsplit(" b/", 1)
35
+ if len(split) == 2:
36
+ a_raw = split[0]
37
+ b_raw = "b/" + split[1]
38
+ return _normalize_git_path(a_raw), _normalize_git_path(b_raw)
39
+
40
+ try:
41
+ parts = shlex.split(payload)
42
+ except ValueError:
43
+ return None, None
44
+ if len(parts) < 2:
45
+ return None, None
46
+ return _normalize_git_path(parts[0]), _normalize_git_path(parts[1])
47
+
48
+
49
+ def _parse_patch_header_path(line: str, prefix: str) -> str | None:
50
+ if not line.startswith(prefix):
51
+ return None
52
+ raw = line[len(prefix) :].split("\t", 1)[0].strip()
53
+ if not raw:
54
+ return None
55
+ path = _normalize_git_path(raw)
56
+ if not path or path == "/dev/null":
57
+ return None
58
+ return path
59
+
60
+
61
+ def iter_patch_paths(patch_text: str) -> list[str]:
62
+ paths: list[str] = []
63
+ seen: set[str] = set()
64
+ saw_diff_header = False
65
+
66
+ for line in patch_text.splitlines():
67
+ a_path, b_path = parse_diff_git_line(line)
68
+ if a_path is None and b_path is None:
69
+ continue
70
+ saw_diff_header = True
71
+ for path in (a_path, b_path):
72
+ if not path or path == "/dev/null" or path in seen:
73
+ continue
74
+ seen.add(path)
75
+ paths.append(path)
76
+
77
+ if saw_diff_header:
78
+ return paths
79
+
80
+ for line in patch_text.splitlines():
81
+ for prefix in ("--- ", "+++ "):
82
+ path = _parse_patch_header_path(line, prefix)
83
+ if path is None or path in seen:
84
+ continue
85
+ seen.add(path)
86
+ paths.append(path)
87
+ return paths
88
+
89
+
90
+ def parse_patch_changed_files(patch_text: str) -> list[str]:
91
+ files: list[str] = []
92
+ seen: set[str] = set()
93
+ saw_diff_header = False
94
+
95
+ for line in patch_text.splitlines():
96
+ a_path, b_path = parse_diff_git_line(line)
97
+ if a_path is None and b_path is None:
98
+ continue
99
+ saw_diff_header = True
100
+ if b_path and b_path != "/dev/null" and b_path not in seen:
101
+ seen.add(b_path)
102
+ files.append(b_path)
103
+
104
+ if saw_diff_header:
105
+ return files
106
+
107
+ for line in patch_text.splitlines():
108
+ path = _parse_patch_header_path(line, "+++ ")
109
+ if path is None or path in seen:
110
+ continue
111
+ seen.add(path)
112
+ files.append(path)
113
+ return files