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
alysis_code/git_ops.py ADDED
@@ -0,0 +1,560 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ import shutil
5
+ import subprocess
6
+ from pathlib import Path
7
+
8
+ from .git_safe import build_git_cmd, build_git_process_env
9
+ from .runtime_artifacts import is_runtime_artifact_path, runtime_artifact_git_exclude_entries
10
+
11
+
12
+ class GitOpsError(RuntimeError):
13
+ pass
14
+
15
+
16
+ DEFAULT_COMMIT_AUTHOR_NAME = "alysis-agent"
17
+ DEFAULT_COMMIT_AUTHOR_EMAIL = "alysis-agent@local"
18
+ _EMPTY_TREE_HASH = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
19
+ _RUNTIME_ARTIFACT_EXCLUDE_BEGIN = "# BEGIN alysis runtime artifacts"
20
+ _RUNTIME_ARTIFACT_EXCLUDE_END = "# END alysis runtime artifacts"
21
+ _LEGACY_BROAD_TARGET_EXCLUDE_ENTRY = "target/"
22
+ _READ_ONLY_GIT_TIMEOUT_S = 5.0
23
+
24
+
25
+ def _git_identity_config(
26
+ *,
27
+ author_name: str = DEFAULT_COMMIT_AUTHOR_NAME,
28
+ author_email: str = DEFAULT_COMMIT_AUTHOR_EMAIL,
29
+ ) -> dict[str, str]:
30
+ return {"user.name": author_name, "user.email": author_email}
31
+
32
+
33
+ def ensure_git_available() -> None:
34
+ if shutil.which("git") is None:
35
+ raise GitOpsError("git is not available on PATH")
36
+
37
+
38
+ def _run_git(
39
+ root: Path,
40
+ args: list[str],
41
+ *,
42
+ input_text: str | None = None,
43
+ extra_config: dict[str, str] | None = None,
44
+ timeout_s: float | None = None,
45
+ ) -> subprocess.CompletedProcess[str]:
46
+ cmd = build_git_cmd(root, args, extra_config=extra_config)
47
+ try:
48
+ return subprocess.run(
49
+ cmd,
50
+ check=False,
51
+ capture_output=True,
52
+ text=True,
53
+ input=input_text,
54
+ env=build_git_process_env(),
55
+ timeout=timeout_s,
56
+ )
57
+ except subprocess.TimeoutExpired:
58
+ return subprocess.CompletedProcess(
59
+ args=cmd,
60
+ returncode=124,
61
+ stdout="",
62
+ stderr="git command timed out",
63
+ )
64
+ except OSError as e:
65
+ raise GitOpsError("failed to run git") from e
66
+
67
+
68
+ def _run_git_checked(
69
+ root: Path,
70
+ args: list[str],
71
+ *,
72
+ input_text: str | None = None,
73
+ extra_config: dict[str, str] | None = None,
74
+ error_message: str,
75
+ timeout_s: float | None = None,
76
+ ) -> subprocess.CompletedProcess[str]:
77
+ cp = _run_git(
78
+ root,
79
+ args,
80
+ input_text=input_text,
81
+ extra_config=extra_config,
82
+ timeout_s=timeout_s,
83
+ )
84
+ if cp.returncode != 0:
85
+ detail = (cp.stderr or cp.stdout).strip()
86
+ raise GitOpsError(f"{error_message}: {detail or 'unknown error'}")
87
+ return cp
88
+
89
+
90
+ def ensure_git_repo(root: Path) -> None:
91
+ _run_git_checked(
92
+ root,
93
+ ["rev-parse", "--git-dir"],
94
+ error_message="not a git repository",
95
+ timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
96
+ )
97
+
98
+
99
+ def current_branch(root: Path) -> str:
100
+ symbolic = _run_git(
101
+ root, ["symbolic-ref", "--short", "HEAD"], timeout_s=_READ_ONLY_GIT_TIMEOUT_S
102
+ )
103
+ if symbolic.returncode == 0:
104
+ branch = symbolic.stdout.strip()
105
+ if branch:
106
+ return branch
107
+
108
+ cp = _run_git_checked(
109
+ root,
110
+ ["rev-parse", "--abbrev-ref", "HEAD"],
111
+ error_message="failed to determine current branch",
112
+ timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
113
+ )
114
+ return cp.stdout.strip()
115
+
116
+
117
+ def has_head_commit(root: Path) -> bool:
118
+ cp = _run_git(root, ["rev-parse", "--verify", "HEAD"], timeout_s=_READ_ONLY_GIT_TIMEOUT_S)
119
+ return cp.returncode == 0
120
+
121
+
122
+ def head_commit(root: Path) -> str | None:
123
+ cp = _run_git(root, ["rev-parse", "HEAD"], timeout_s=_READ_ONLY_GIT_TIMEOUT_S)
124
+ if cp.returncode != 0:
125
+ return None
126
+ value = (cp.stdout or "").strip()
127
+ return value or None
128
+
129
+
130
+ def git_path(root: Path, pathspec: str) -> Path:
131
+ cp = _run_git_checked(
132
+ root,
133
+ ["rev-parse", "--git-path", pathspec],
134
+ error_message=f"failed to resolve git path for {pathspec}",
135
+ timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
136
+ )
137
+ resolved = (cp.stdout or "").strip()
138
+ if not resolved:
139
+ raise GitOpsError(f"failed to resolve git path for {pathspec}: empty result")
140
+ path = Path(resolved)
141
+ if not path.is_absolute():
142
+ path = root / path
143
+ return path
144
+
145
+
146
+ def ensure_info_exclude_entries(root: Path, entries: list[str]) -> Path:
147
+ exclude_path = git_path(root, "info/exclude")
148
+ exclude_path.parent.mkdir(parents=True, exist_ok=True)
149
+
150
+ normalized_entries = [entry.strip() for entry in entries if entry.strip()]
151
+ if not normalized_entries:
152
+ return exclude_path
153
+
154
+ if exclude_path.exists():
155
+ raw_lines = exclude_path.read_text(encoding="utf-8").splitlines()
156
+ else:
157
+ raw_lines = []
158
+
159
+ existing = {line.strip() for line in raw_lines if line.strip()}
160
+ to_append = [entry for entry in normalized_entries if entry not in existing]
161
+ if not to_append:
162
+ return exclude_path
163
+
164
+ with exclude_path.open("a", encoding="utf-8") as fh:
165
+ for entry in to_append:
166
+ fh.write(entry + "\n")
167
+ return exclude_path
168
+
169
+
170
+ def ensure_runtime_artifact_excludes(root: Path) -> Path:
171
+ exclude_path = git_path(root, "info/exclude")
172
+ exclude_path.parent.mkdir(parents=True, exist_ok=True)
173
+
174
+ desired_entries = list(runtime_artifact_git_exclude_entries(root))
175
+ if exclude_path.exists():
176
+ raw_lines = exclude_path.read_text(encoding="utf-8").splitlines()
177
+ else:
178
+ raw_lines = []
179
+
180
+ preserved_lines: list[str] = []
181
+ inside_managed_block = False
182
+ for line in raw_lines:
183
+ stripped = line.strip()
184
+ if stripped == _RUNTIME_ARTIFACT_EXCLUDE_BEGIN:
185
+ inside_managed_block = True
186
+ continue
187
+ if stripped == _RUNTIME_ARTIFACT_EXCLUDE_END:
188
+ inside_managed_block = False
189
+ continue
190
+ if inside_managed_block:
191
+ continue
192
+ if stripped == _LEGACY_BROAD_TARGET_EXCLUDE_ENTRY:
193
+ continue
194
+ preserved_lines.append(line)
195
+
196
+ while preserved_lines and not preserved_lines[-1].strip():
197
+ preserved_lines.pop()
198
+ if preserved_lines:
199
+ preserved_lines.append("")
200
+ preserved_lines.extend(
201
+ [
202
+ _RUNTIME_ARTIFACT_EXCLUDE_BEGIN,
203
+ *desired_entries,
204
+ _RUNTIME_ARTIFACT_EXCLUDE_END,
205
+ ]
206
+ )
207
+ exclude_path.write_text("\n".join(preserved_lines) + "\n", encoding="utf-8")
208
+ return exclude_path
209
+
210
+
211
+ def ensure_clean_for_pr(root: Path) -> None:
212
+ staged = _run_git_checked(
213
+ root,
214
+ ["diff", "--cached", "--name-only"],
215
+ error_message="failed to inspect staged changes",
216
+ timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
217
+ ).stdout.strip()
218
+ if staged:
219
+ raise GitOpsError("working tree is not clean: staged changes exist")
220
+
221
+ unstaged = _run_git_checked(
222
+ root,
223
+ ["diff", "--name-only"],
224
+ error_message="failed to inspect unstaged changes",
225
+ timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
226
+ ).stdout.strip()
227
+ if unstaged:
228
+ raise GitOpsError("working tree is not clean: unstaged tracked changes exist")
229
+
230
+ # Only untracked-and-not-ignored files appear with --exclude-standard.
231
+ ensure_runtime_artifact_excludes(root)
232
+ untracked = _run_git_checked(
233
+ root,
234
+ ["ls-files", "--others", "--exclude-standard"],
235
+ error_message="failed to inspect untracked files",
236
+ timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
237
+ ).stdout.strip()
238
+ if untracked:
239
+ raise GitOpsError(
240
+ "working tree has untracked files not ignored by git; please clean them first"
241
+ )
242
+
243
+
244
+ def branch_exists(root: Path, branch: str) -> bool:
245
+ cp = _run_git(
246
+ root,
247
+ ["show-ref", "--verify", "--quiet", f"refs/heads/{branch}"],
248
+ timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
249
+ )
250
+ return cp.returncode == 0
251
+
252
+
253
+ def status_porcelain(root: Path) -> list[str]:
254
+ cp = _run_git_checked(
255
+ root,
256
+ ["status", "--porcelain=v1", "--untracked-files=normal"],
257
+ error_message="failed to inspect worktree status",
258
+ timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
259
+ )
260
+ return [line.rstrip() for line in cp.stdout.splitlines() if line.strip()]
261
+
262
+
263
+ def untracked_files(root: Path) -> list[str]:
264
+ ensure_runtime_artifact_excludes(root)
265
+ cp = _run_git_checked(
266
+ root,
267
+ ["ls-files", "--others", "--exclude-standard"],
268
+ error_message="failed to inspect untracked files",
269
+ timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
270
+ )
271
+ return [line.strip() for line in cp.stdout.splitlines() if line.strip()]
272
+
273
+
274
+ def reset_hard(root: Path, *, target: str = "HEAD") -> None:
275
+ _run_git_checked(
276
+ root,
277
+ ["reset", "--hard", target],
278
+ error_message=f"failed to reset worktree to {target}",
279
+ )
280
+
281
+
282
+ def clean_untracked(root: Path) -> None:
283
+ _run_git_checked(
284
+ root,
285
+ ["clean", "-fd"],
286
+ error_message="failed to clean untracked files",
287
+ )
288
+
289
+
290
+ def checkout_branch(root: Path, branch: str, *, base_branch: str) -> None:
291
+ if branch_exists(root, branch):
292
+ _run_git_checked(root, ["checkout", branch], error_message=f"failed to checkout {branch}")
293
+ return
294
+ _run_git_checked(
295
+ root,
296
+ ["checkout", "-b", branch, base_branch],
297
+ error_message=f"failed to create branch {branch}",
298
+ )
299
+
300
+
301
+ def stage_all(root: Path) -> None:
302
+ _run_git_checked(root, ["add", "-A"], error_message="failed to stage changes")
303
+
304
+
305
+ def staged_files(root: Path) -> list[str]:
306
+ cp = _run_git_checked(
307
+ root,
308
+ ["diff", "--cached", "--name-only"],
309
+ error_message="failed to list staged files",
310
+ timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
311
+ )
312
+ return [line.strip() for line in cp.stdout.splitlines() if line.strip()]
313
+
314
+
315
+ def _staged_runtime_artifact_paths(
316
+ root: Path,
317
+ *,
318
+ current_paths: list[str] | None = None,
319
+ ) -> list[str]:
320
+ staged = current_paths if current_paths is not None else staged_files(root)
321
+ return [path for path in staged if is_runtime_artifact_path(path, root=root)]
322
+
323
+
324
+ def _normalize_rel_prefix(value: str) -> str:
325
+ cleaned = value.strip().replace("\\", "/")
326
+ while cleaned.startswith("./"):
327
+ cleaned = cleaned[2:]
328
+ return cleaned.rstrip("/")
329
+
330
+
331
+ def _matches_prefix(rel_path: str, prefix: str) -> bool:
332
+ return rel_path == prefix or rel_path.startswith(prefix + "/")
333
+
334
+
335
+ def unstage_staged_prefixes(root: Path, prefixes: list[str]) -> list[str]:
336
+ current = staged_files(root)
337
+ normalized = [_normalize_rel_prefix(p) for p in prefixes if p.strip()]
338
+ if not normalized:
339
+ return []
340
+
341
+ to_unstage: list[str] = []
342
+ for path in current:
343
+ rel = _normalize_rel_prefix(path)
344
+ if any(_matches_prefix(rel, pref) for pref in normalized):
345
+ to_unstage.append(path)
346
+
347
+ if not to_unstage:
348
+ return []
349
+
350
+ _run_git_checked(
351
+ root,
352
+ ["reset", "HEAD", "--", *to_unstage],
353
+ error_message="failed to unstage protected paths",
354
+ )
355
+ return to_unstage
356
+
357
+
358
+ def unstage_staged_paths(root: Path, paths: list[str]) -> list[str]:
359
+ normalized = {_normalize_rel_prefix(path) for path in paths if path.strip()}
360
+ if not normalized:
361
+ return []
362
+
363
+ current = staged_files(root)
364
+ to_unstage = [path for path in current if _normalize_rel_prefix(path) in normalized]
365
+ if not to_unstage:
366
+ return []
367
+
368
+ _run_git_checked(
369
+ root,
370
+ ["reset", "HEAD", "--", *to_unstage],
371
+ error_message="failed to unstage filtered files",
372
+ )
373
+ return to_unstage
374
+
375
+
376
+ def unstage_staged_runtime_artifacts(
377
+ root: Path,
378
+ *,
379
+ current_paths: list[str] | None = None,
380
+ ) -> list[str]:
381
+ return unstage_staged_paths(
382
+ root,
383
+ _staged_runtime_artifact_paths(root, current_paths=current_paths),
384
+ )
385
+
386
+
387
+ def ensure_not_staged_prefixes(root: Path, prefixes: list[str]) -> None:
388
+ current = staged_files(root)
389
+ normalized = [_normalize_rel_prefix(p) for p in prefixes if p.strip()]
390
+ for path in current:
391
+ rel = _normalize_rel_prefix(path)
392
+ for pref in normalized:
393
+ if _matches_prefix(rel, pref):
394
+ raise GitOpsError(f"staged file under protected path: {path}")
395
+
396
+
397
+ def ensure_not_staged_paths(root: Path, paths: list[str]) -> None:
398
+ current = staged_files(root)
399
+ normalized = {_normalize_rel_prefix(path) for path in paths if path.strip()}
400
+ for path in current:
401
+ if _normalize_rel_prefix(path) in normalized:
402
+ raise GitOpsError(f"staged filtered file: {path}")
403
+
404
+
405
+ def ensure_not_staged_runtime_artifacts(
406
+ root: Path,
407
+ *,
408
+ current_paths: list[str] | None = None,
409
+ ) -> None:
410
+ current = _staged_runtime_artifact_paths(root, current_paths=current_paths)
411
+ if current:
412
+ preview = ", ".join(current[:20])
413
+ if len(current) > 20:
414
+ preview += ", ..."
415
+ raise GitOpsError(f"staged runtime artifact path: {preview}")
416
+
417
+
418
+ def commit_all(
419
+ root: Path,
420
+ *,
421
+ message: str,
422
+ author_name: str = DEFAULT_COMMIT_AUTHOR_NAME,
423
+ author_email: str = DEFAULT_COMMIT_AUTHOR_EMAIL,
424
+ ) -> str:
425
+ _run_git_checked(
426
+ root,
427
+ ["commit", "-m", message],
428
+ extra_config=_git_identity_config(
429
+ author_name=author_name,
430
+ author_email=author_email,
431
+ ),
432
+ error_message="failed to create commit",
433
+ )
434
+ cp = _run_git_checked(
435
+ root,
436
+ ["rev-parse", "HEAD"],
437
+ error_message="failed to resolve commit hash",
438
+ )
439
+ return cp.stdout.strip()
440
+
441
+
442
+ def format_patch_stdout(root: Path, *, base_branch: str) -> str:
443
+ cp = _run_git_checked(
444
+ root,
445
+ ["format-patch", f"{base_branch}..HEAD", "--stdout"],
446
+ error_message="failed to generate patch",
447
+ timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
448
+ )
449
+ return cp.stdout
450
+
451
+
452
+ def diff_text_since(
453
+ root: Path,
454
+ *,
455
+ before_commit: str | None,
456
+ after_commit: str | None,
457
+ ) -> str:
458
+ if not after_commit or after_commit == before_commit:
459
+ return ""
460
+ base = before_commit or _EMPTY_TREE_HASH
461
+ cp = _run_git_checked(
462
+ root,
463
+ ["diff", "--binary", "-M", base, after_commit],
464
+ error_message="failed to generate committed diff",
465
+ timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
466
+ )
467
+ return cp.stdout
468
+
469
+
470
+ def changed_files_between(root: Path, *, revspec: str) -> list[str]:
471
+ cp = _run_git_checked(
472
+ root,
473
+ ["diff", "--name-only", revspec],
474
+ error_message=f"failed to list changed files for {revspec}",
475
+ timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
476
+ )
477
+ return [line.strip() for line in cp.stdout.splitlines() if line.strip()]
478
+
479
+
480
+ def changed_files_since(
481
+ root: Path,
482
+ *,
483
+ before_commit: str | None,
484
+ after_commit: str | None,
485
+ ) -> list[str]:
486
+ if not after_commit or after_commit == before_commit:
487
+ return []
488
+ base = before_commit or _EMPTY_TREE_HASH
489
+ cp = _run_git_checked(
490
+ root,
491
+ ["diff", "--name-only", base, after_commit],
492
+ error_message="failed to list committed changed files",
493
+ timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
494
+ )
495
+ return [line.strip() for line in cp.stdout.splitlines() if line.strip()]
496
+
497
+
498
+ def added_files_since(
499
+ root: Path,
500
+ *,
501
+ before_commit: str | None,
502
+ after_commit: str | None,
503
+ ) -> list[str]:
504
+ if not after_commit or after_commit == before_commit:
505
+ return []
506
+ base = before_commit or _EMPTY_TREE_HASH
507
+ cp = _run_git_checked(
508
+ root,
509
+ ["diff", "--diff-filter=A", "--name-only", base, after_commit],
510
+ error_message="failed to list added committed files",
511
+ timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
512
+ )
513
+ return [line.strip() for line in cp.stdout.splitlines() if line.strip()]
514
+
515
+
516
+ def reset_mixed(root: Path, *, target: str = "HEAD") -> None:
517
+ _run_git_checked(
518
+ root,
519
+ ["reset", "--mixed", target],
520
+ error_message=f"failed to reset worktree to {target} while preserving changes",
521
+ )
522
+
523
+
524
+ def merge_no_ff(root: Path, *, base_branch: str, task_branch: str, message: str) -> str:
525
+ _run_git_checked(
526
+ root,
527
+ ["checkout", base_branch],
528
+ error_message=f"failed to checkout base branch {base_branch}",
529
+ )
530
+ _run_git_checked(
531
+ root,
532
+ ["merge", "--no-ff", task_branch, "-m", message],
533
+ extra_config=_git_identity_config(),
534
+ error_message=f"failed to merge branch {task_branch}",
535
+ )
536
+ cp = _run_git_checked(
537
+ root,
538
+ ["rev-parse", "HEAD"],
539
+ error_message="failed to resolve merge commit hash",
540
+ )
541
+ return cp.stdout.strip()
542
+
543
+
544
+ def delete_branch(root: Path, branch: str) -> None:
545
+ _run_git_checked(
546
+ root,
547
+ ["branch", "-d", branch],
548
+ error_message=f"failed to delete branch {branch}",
549
+ )
550
+
551
+
552
+ def generate_task_branch_name(task_id: str, title: str) -> str:
553
+ tid = task_id.strip().lower() or "task"
554
+ slug = re.sub(r"[^a-z0-9]+", "-", title.strip().lower())
555
+ slug = slug.strip("-")
556
+ if not slug:
557
+ slug = "task"
558
+ if len(slug) > 40:
559
+ slug = slug[:40].rstrip("-")
560
+ return f"feat/{tid}-{slug}"
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import os
5
+ import tempfile
6
+ from collections.abc import Mapping
7
+ from pathlib import Path
8
+
9
+ from .branding import env_get_from
10
+
11
+ _TRUE_VALUES = {"1", "true", "yes", "on", "enable", "enabled"}
12
+ _NON_INTERACTIVE_GIT_ENV = {
13
+ "GIT_TERMINAL_PROMPT": "0",
14
+ "GIT_ASKPASS": "",
15
+ "SSH_ASKPASS": "",
16
+ "GCM_INTERACTIVE": "never",
17
+ "GIT_EDITOR": "true",
18
+ "GIT_MERGE_AUTOEDIT": "no",
19
+ "PAGER": "cat",
20
+ }
21
+
22
+
23
+ def git_hooks_enabled(env: Mapping[str, str] | None = None) -> bool:
24
+ source = env if env is not None else os.environ
25
+ raw = str(env_get_from(source, "ALYSIS_GIT_HOOKS") or "").strip().lower()
26
+ return raw in _TRUE_VALUES
27
+
28
+
29
+ def resolve_disabled_hooks_dir(root: Path, env: Mapping[str, str] | None = None) -> Path:
30
+ source = env if env is not None else os.environ
31
+ override = str(env_get_from(source, "ALYSIS_GIT_HOOKS_PATH") or "").strip()
32
+ if override:
33
+ target = Path(override).expanduser()
34
+ else:
35
+ root_hash = hashlib.sha256(os.fspath(root.resolve()).encode("utf-8")).hexdigest()[:16]
36
+ target = Path(tempfile.gettempdir()) / "alysis-agent" / "hooks-disabled" / root_hash
37
+ target.mkdir(parents=True, exist_ok=True)
38
+ return target
39
+
40
+
41
+ def build_git_process_env(env: Mapping[str, str] | None = None) -> dict[str, str]:
42
+ source = dict(os.environ if env is None else env)
43
+ source.update(_NON_INTERACTIVE_GIT_ENV)
44
+ return source
45
+
46
+
47
+ def build_git_cmd(
48
+ root: Path,
49
+ args: list[str],
50
+ *,
51
+ extra_config: dict[str, str] | None = None,
52
+ env: Mapping[str, str] | None = None,
53
+ ) -> list[str]:
54
+ config = dict(extra_config or {})
55
+ if not git_hooks_enabled(env):
56
+ config.setdefault("core.hooksPath", os.fspath(resolve_disabled_hooks_dir(root, env)))
57
+
58
+ cmd: list[str] = ["git", "-C", os.fspath(root)]
59
+ for key, value in config.items():
60
+ cmd.extend(["-c", f"{key}={value}"])
61
+ cmd.extend(args)
62
+ return cmd