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,408 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from collections.abc import Callable
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import typer
9
+
10
+ from .workspace_binding import (
11
+ WorkspaceAction,
12
+ WorkspaceBinding,
13
+ WorkspaceBindingError,
14
+ WorkspaceCandidate,
15
+ WorkspaceRiskLevel,
16
+ discover_workspace_candidates,
17
+ ensure_workspace_policy,
18
+ resolve_workspace_binding,
19
+ workspace_policy,
20
+ workspace_policy_violation_message,
21
+ )
22
+
23
+ _ACTION_USE_CURRENT = "use_current"
24
+ _ACTION_CHOOSE_PROJECT = "choose_project"
25
+ _ACTION_CREATE_FOLDER = "create_folder"
26
+ _ACTION_ENTER_PATH = "enter_path"
27
+ _ACTION_CANCEL = "cancel"
28
+ _DEFAULT_ACTION = _ACTION_CHOOSE_PROJECT
29
+
30
+
31
+ def guarded_workspace_action_rows(
32
+ *,
33
+ binding: WorkspaceBinding,
34
+ candidates: tuple[WorkspaceCandidate, ...],
35
+ allow_use_current_action: bool = True,
36
+ ) -> list[tuple[str, str, str]]:
37
+ current_label = _display_path(binding.requested_path)
38
+ rows: list[tuple[str, str, str]] = []
39
+ if allow_use_current_action:
40
+ rows.append(
41
+ (
42
+ _ACTION_USE_CURRENT,
43
+ "1) use current directory anyway",
44
+ f"Bind to {current_label} despite the guarded risk.",
45
+ )
46
+ )
47
+ choose_label = "2) choose an existing project folder"
48
+ create_label = "3) create a new folder here"
49
+ enter_label = "4) enter another path"
50
+ cancel_label = "5) cancel"
51
+ else:
52
+ choose_label = "1) choose an existing project folder"
53
+ create_label = "2) create a new folder here"
54
+ enter_label = "3) enter another path"
55
+ cancel_label = "4) cancel"
56
+ rows.extend(
57
+ [
58
+ (
59
+ _ACTION_CHOOSE_PROJECT,
60
+ choose_label,
61
+ (
62
+ f"Pick from {len(candidates)} shallow project candidate(s)."
63
+ if candidates
64
+ else "Pick another existing folder manually."
65
+ ),
66
+ ),
67
+ (
68
+ _ACTION_CREATE_FOLDER,
69
+ create_label,
70
+ f"Create a new child directory under {current_label} and bind to it.",
71
+ ),
72
+ (
73
+ _ACTION_ENTER_PATH,
74
+ enter_label,
75
+ "Type an explicit existing path or a relative child path.",
76
+ ),
77
+ (
78
+ _ACTION_CANCEL,
79
+ cancel_label,
80
+ "Abort startup without creating a session.",
81
+ ),
82
+ ]
83
+ )
84
+ return rows
85
+
86
+
87
+ def workspace_candidate_rows(
88
+ *,
89
+ base_path: Path,
90
+ candidates: tuple[WorkspaceCandidate, ...],
91
+ ) -> list[tuple[str, str, str]]:
92
+ rows: list[tuple[str, str, str]] = []
93
+ for idx, candidate in enumerate(candidates, start=1):
94
+ try:
95
+ label_path = os.fspath(candidate.path.relative_to(base_path))
96
+ except ValueError:
97
+ label_path = _display_path(candidate.path)
98
+ rows.append(
99
+ (
100
+ os.fspath(candidate.path),
101
+ f"{idx}) {label_path}",
102
+ candidate.summary,
103
+ )
104
+ )
105
+ return rows
106
+
107
+
108
+ def resolve_startup_workspace_binding(
109
+ *,
110
+ requested_path: Path,
111
+ interactive: bool,
112
+ create_if_missing: bool = False,
113
+ allow_broad_workspace: bool = False,
114
+ source: str = "explicit_path",
115
+ action: str = WorkspaceAction.CHAT,
116
+ console: Any | None = None,
117
+ select_action_interactive: Callable[..., tuple[str | None, bool]] | None = None,
118
+ select_candidate_interactive: Callable[..., tuple[Path | None, bool]] | None = None,
119
+ prompt_text: Callable[..., str] | None = None,
120
+ ) -> WorkspaceBinding:
121
+ prompter = prompt_text or typer.prompt
122
+ policy = workspace_policy(action)
123
+ binding = resolve_workspace_binding(
124
+ requested_path,
125
+ create_if_missing=create_if_missing,
126
+ allow_broad_workspace=allow_broad_workspace,
127
+ source=source,
128
+ )
129
+ try:
130
+ return ensure_workspace_policy(
131
+ binding,
132
+ action=policy.action,
133
+ allow_broad_workspace=allow_broad_workspace,
134
+ )
135
+ except WorkspaceBindingError:
136
+ pass
137
+ if binding.risk_level == WorkspaceRiskLevel.BLOCKED:
138
+ raise WorkspaceBindingError(
139
+ workspace_policy_violation_message(binding, action=policy.action)
140
+ )
141
+ if not interactive or not policy.interactive_guarded_resolution:
142
+ raise WorkspaceBindingError(
143
+ workspace_policy_violation_message(binding, action=policy.action)
144
+ )
145
+ return _resolve_guarded_binding_interactively(
146
+ binding=binding,
147
+ policy_action=policy.action,
148
+ allow_use_current_action=policy.interactive_allow_use_current,
149
+ console=console,
150
+ select_action_interactive=select_action_interactive,
151
+ select_candidate_interactive=select_candidate_interactive,
152
+ prompt_text=prompter,
153
+ )
154
+
155
+
156
+ def _resolve_guarded_binding_interactively(
157
+ *,
158
+ binding: WorkspaceBinding,
159
+ policy_action: str,
160
+ allow_use_current_action: bool,
161
+ console: Any | None,
162
+ select_action_interactive: Callable[..., tuple[str | None, bool]] | None,
163
+ select_candidate_interactive: Callable[..., tuple[Path | None, bool]] | None,
164
+ prompt_text: Callable[..., str],
165
+ ) -> WorkspaceBinding:
166
+ while True:
167
+ candidates = discover_workspace_candidates(binding.workspace_context.workspace_root)
168
+ selected_action = _choose_guarded_action(
169
+ binding=binding,
170
+ candidates=candidates,
171
+ allow_use_current_action=allow_use_current_action,
172
+ console=console,
173
+ select_action_interactive=select_action_interactive,
174
+ prompt_text=prompt_text,
175
+ )
176
+ if selected_action == _ACTION_CANCEL:
177
+ raise WorkspaceBindingError("Workspace binding cancelled.")
178
+ if selected_action == _ACTION_USE_CURRENT:
179
+ if not allow_use_current_action:
180
+ raise WorkspaceBindingError(
181
+ "This command requires a narrower workspace. "
182
+ "Pass --allow-broad-workspace to continue."
183
+ )
184
+ return resolve_workspace_binding(
185
+ binding.requested_path,
186
+ allow_broad_workspace=True,
187
+ source=binding.binding_source,
188
+ )
189
+ if selected_action == _ACTION_CHOOSE_PROJECT:
190
+ selected_path = _choose_workspace_candidate(
191
+ base_path=binding.workspace_context.workspace_root,
192
+ candidates=candidates,
193
+ console=console,
194
+ select_candidate_interactive=select_candidate_interactive,
195
+ prompt_text=prompt_text,
196
+ )
197
+ if selected_path is None:
198
+ continue
199
+ return resolve_startup_workspace_binding(
200
+ requested_path=selected_path,
201
+ interactive=True,
202
+ create_if_missing=False,
203
+ allow_broad_workspace=False,
204
+ source="startup_candidate",
205
+ action=policy_action,
206
+ console=console,
207
+ select_action_interactive=select_action_interactive,
208
+ select_candidate_interactive=select_candidate_interactive,
209
+ prompt_text=prompt_text,
210
+ )
211
+ if selected_action == _ACTION_CREATE_FOLDER:
212
+ created_path = _prompt_new_child_path(
213
+ base_path=binding.workspace_context.workspace_root,
214
+ prompt_text=prompt_text,
215
+ )
216
+ if created_path is None:
217
+ continue
218
+ return resolve_startup_workspace_binding(
219
+ requested_path=created_path,
220
+ interactive=True,
221
+ create_if_missing=True,
222
+ allow_broad_workspace=False,
223
+ source="startup_create_path",
224
+ action=policy_action,
225
+ console=console,
226
+ select_action_interactive=select_action_interactive,
227
+ select_candidate_interactive=select_candidate_interactive,
228
+ prompt_text=prompt_text,
229
+ )
230
+ if selected_action == _ACTION_ENTER_PATH:
231
+ entered_path = _prompt_workspace_path(
232
+ base_path=binding.workspace_context.workspace_root,
233
+ prompt_text=prompt_text,
234
+ )
235
+ if entered_path is None:
236
+ continue
237
+ return resolve_startup_workspace_binding(
238
+ requested_path=entered_path,
239
+ interactive=True,
240
+ create_if_missing=False,
241
+ allow_broad_workspace=False,
242
+ source="startup_entered_path",
243
+ action=policy_action,
244
+ console=console,
245
+ select_action_interactive=select_action_interactive,
246
+ select_candidate_interactive=select_candidate_interactive,
247
+ prompt_text=prompt_text,
248
+ )
249
+
250
+
251
+ def _choose_guarded_action(
252
+ *,
253
+ binding: WorkspaceBinding,
254
+ candidates: tuple[WorkspaceCandidate, ...],
255
+ allow_use_current_action: bool,
256
+ console: Any | None,
257
+ select_action_interactive: Callable[..., tuple[str | None, bool]] | None,
258
+ prompt_text: Callable[..., str],
259
+ ) -> str:
260
+ if callable(select_action_interactive):
261
+ selected, interactive_available = select_action_interactive(
262
+ binding=binding,
263
+ candidates=candidates,
264
+ allow_use_current_action=allow_use_current_action,
265
+ console=console,
266
+ )
267
+ if interactive_available and selected is not None:
268
+ return selected
269
+ if allow_use_current_action:
270
+ prompt = "Workspace action [1=use current|2=choose project|3=create folder|4=enter path|5=cancel]"
271
+ default = "2" if candidates else "3"
272
+ else:
273
+ prompt = "Workspace action [1=choose project|2=create folder|3=enter path|4=cancel]"
274
+ default = "1" if candidates else "2"
275
+ try:
276
+ raw = prompt_text(
277
+ prompt,
278
+ default=default,
279
+ )
280
+ except (EOFError, KeyboardInterrupt):
281
+ return _ACTION_CANCEL
282
+ return _normalize_guarded_action(raw, allow_use_current_action=allow_use_current_action)
283
+
284
+
285
+ def _choose_workspace_candidate(
286
+ *,
287
+ base_path: Path,
288
+ candidates: tuple[WorkspaceCandidate, ...],
289
+ console: Any | None,
290
+ select_candidate_interactive: Callable[..., tuple[Path | None, bool]] | None,
291
+ prompt_text: Callable[..., str],
292
+ ) -> Path | None:
293
+ if not candidates:
294
+ typed_path = _prompt_workspace_path(base_path=base_path, prompt_text=prompt_text)
295
+ return typed_path
296
+ if callable(select_candidate_interactive):
297
+ selected, interactive_available = select_candidate_interactive(
298
+ base_path=base_path,
299
+ candidates=candidates,
300
+ console=console,
301
+ )
302
+ if interactive_available:
303
+ return selected
304
+
305
+ rows = workspace_candidate_rows(base_path=base_path, candidates=candidates)
306
+ try:
307
+ raw = prompt_text(
308
+ f"Project folder [1-{len(rows)} or blank to cancel]",
309
+ default="1",
310
+ )
311
+ except (EOFError, KeyboardInterrupt):
312
+ return None
313
+ text = str(raw or "").strip()
314
+ if not text:
315
+ return None
316
+ if text.isdigit():
317
+ index = int(text)
318
+ if 1 <= index <= len(rows):
319
+ return Path(rows[index - 1][0])
320
+ raise WorkspaceBindingError("Invalid project selection.")
321
+
322
+
323
+ def _normalize_guarded_action(raw: str, *, allow_use_current_action: bool = True) -> str:
324
+ value = str(raw or "").strip().casefold()
325
+ if allow_use_current_action:
326
+ aliases = {
327
+ "1": _ACTION_USE_CURRENT,
328
+ "use": _ACTION_USE_CURRENT,
329
+ "use_current": _ACTION_USE_CURRENT,
330
+ "current": _ACTION_USE_CURRENT,
331
+ "2": _ACTION_CHOOSE_PROJECT,
332
+ "choose": _ACTION_CHOOSE_PROJECT,
333
+ "project": _ACTION_CHOOSE_PROJECT,
334
+ "choose_project": _ACTION_CHOOSE_PROJECT,
335
+ "3": _ACTION_CREATE_FOLDER,
336
+ "create": _ACTION_CREATE_FOLDER,
337
+ "new": _ACTION_CREATE_FOLDER,
338
+ "create_folder": _ACTION_CREATE_FOLDER,
339
+ "4": _ACTION_ENTER_PATH,
340
+ "enter": _ACTION_ENTER_PATH,
341
+ "path": _ACTION_ENTER_PATH,
342
+ "enter_path": _ACTION_ENTER_PATH,
343
+ "5": _ACTION_CANCEL,
344
+ "cancel": _ACTION_CANCEL,
345
+ "quit": _ACTION_CANCEL,
346
+ }
347
+ else:
348
+ aliases = {
349
+ "1": _ACTION_CHOOSE_PROJECT,
350
+ "choose": _ACTION_CHOOSE_PROJECT,
351
+ "project": _ACTION_CHOOSE_PROJECT,
352
+ "choose_project": _ACTION_CHOOSE_PROJECT,
353
+ "2": _ACTION_CREATE_FOLDER,
354
+ "create": _ACTION_CREATE_FOLDER,
355
+ "new": _ACTION_CREATE_FOLDER,
356
+ "create_folder": _ACTION_CREATE_FOLDER,
357
+ "3": _ACTION_ENTER_PATH,
358
+ "enter": _ACTION_ENTER_PATH,
359
+ "path": _ACTION_ENTER_PATH,
360
+ "enter_path": _ACTION_ENTER_PATH,
361
+ "4": _ACTION_CANCEL,
362
+ "cancel": _ACTION_CANCEL,
363
+ "quit": _ACTION_CANCEL,
364
+ }
365
+ return aliases.get(value, _DEFAULT_ACTION)
366
+
367
+
368
+ def _prompt_new_child_path(*, base_path: Path, prompt_text: Callable[..., str]) -> Path | None:
369
+ try:
370
+ raw = prompt_text("New folder name", default="new-project")
371
+ except (EOFError, KeyboardInterrupt):
372
+ return None
373
+ text = str(raw or "").strip()
374
+ if not text:
375
+ return None
376
+ rel = Path(text)
377
+ if rel.is_absolute():
378
+ raise WorkspaceBindingError("New folder name must be relative to the current directory.")
379
+ target = (base_path / rel).expanduser().resolve(strict=False)
380
+ _ensure_within_base(base_path=base_path, candidate=target)
381
+ return target
382
+
383
+
384
+ def _prompt_workspace_path(*, base_path: Path, prompt_text: Callable[..., str]) -> Path | None:
385
+ try:
386
+ raw = prompt_text("Workspace path")
387
+ except (EOFError, KeyboardInterrupt):
388
+ return None
389
+ text = str(raw or "").strip()
390
+ if not text:
391
+ return None
392
+ candidate = Path(text).expanduser()
393
+ if not candidate.is_absolute():
394
+ candidate = base_path / candidate
395
+ return candidate.resolve(strict=False)
396
+
397
+
398
+ def _ensure_within_base(*, base_path: Path, candidate: Path) -> None:
399
+ try:
400
+ candidate.relative_to(base_path.resolve())
401
+ except ValueError as e:
402
+ raise WorkspaceBindingError(
403
+ f"Path must stay under {base_path.resolve()}: {candidate}"
404
+ ) from e
405
+
406
+
407
+ def _display_path(path: Path) -> str:
408
+ return os.fspath(path.expanduser().resolve(strict=False))
@@ -0,0 +1,273 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import re
5
+ import subprocess
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ from .git_safe import build_git_process_env
10
+
11
+ _HEAD_REF_PREFIX = "ref:"
12
+ _HEADS_PREFIX = "refs/heads/"
13
+ _OID_RE = re.compile(r"^[0-9a-fA-F]{40,64}$")
14
+ _GIT_PROBE_TIMEOUT_S = 2.0
15
+
16
+ WORKSPACE_KIND_GIT_REPO = "git_repo"
17
+ WORKSPACE_KIND_GIT_REPO_NO_HEAD = "git_repo_no_head"
18
+ WORKSPACE_KIND_PLAIN_DIR = "plain_dir"
19
+
20
+
21
+ class WorkspaceContextError(RuntimeError):
22
+ pass
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class WorkspaceContext:
27
+ input_path: Path
28
+ focus_path: Path
29
+ workspace_root: Path
30
+ git_root: Path | None
31
+ focus_relpath: str
32
+ workspace_kind: str
33
+ has_head_commit: bool
34
+ current_branch: str | None
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class _GitResolution:
39
+ workspace_root: Path
40
+ git_root: Path
41
+ has_head_commit: bool
42
+ current_branch: str | None
43
+
44
+
45
+ def resolve_workspace_context(path: Path) -> WorkspaceContext:
46
+ """Resolve workspace metadata for an existing directory without mutating the filesystem."""
47
+ input_path = _resolve_input_path(path)
48
+ git_resolution = _resolve_git_resolution(input_path)
49
+ if git_resolution is None:
50
+ return WorkspaceContext(
51
+ input_path=input_path,
52
+ focus_path=input_path,
53
+ workspace_root=input_path,
54
+ git_root=None,
55
+ focus_relpath=".",
56
+ workspace_kind=WORKSPACE_KIND_PLAIN_DIR,
57
+ has_head_commit=False,
58
+ current_branch=None,
59
+ )
60
+
61
+ workspace_root = git_resolution.workspace_root
62
+ focus_relpath = "."
63
+ if input_path != workspace_root:
64
+ focus_relpath = input_path.relative_to(workspace_root).as_posix()
65
+ return WorkspaceContext(
66
+ input_path=input_path,
67
+ focus_path=input_path,
68
+ workspace_root=workspace_root,
69
+ git_root=git_resolution.git_root,
70
+ focus_relpath=focus_relpath,
71
+ workspace_kind=(
72
+ WORKSPACE_KIND_GIT_REPO
73
+ if git_resolution.has_head_commit
74
+ else WORKSPACE_KIND_GIT_REPO_NO_HEAD
75
+ ),
76
+ has_head_commit=git_resolution.has_head_commit,
77
+ current_branch=git_resolution.current_branch,
78
+ )
79
+
80
+
81
+ def _resolve_input_path(path: Path) -> Path:
82
+ resolved = path.expanduser().resolve()
83
+ if not resolved.exists():
84
+ raise WorkspaceContextError(f"Workspace path does not exist: {resolved}")
85
+ if not resolved.is_dir():
86
+ raise WorkspaceContextError(f"Workspace path is not a directory: {resolved}")
87
+ return resolved
88
+
89
+
90
+ def _resolve_git_resolution(path: Path) -> _GitResolution | None:
91
+ fs_root = _find_git_root_from_filesystem(path)
92
+ if fs_root is None:
93
+ return None
94
+ git_root = _git_show_toplevel(path)
95
+ if git_root is not None:
96
+ return _git_resolution_from_root(git_root)
97
+ return _git_resolution_from_root(fs_root)
98
+
99
+
100
+ def _git_show_toplevel(path: Path) -> Path | None:
101
+ cp = _run_git(path, ["rev-parse", "--show-toplevel"])
102
+ if cp is None or cp.returncode != 0:
103
+ return None
104
+ value = (cp.stdout or "").strip()
105
+ if not value:
106
+ return None
107
+ try:
108
+ return Path(value).resolve()
109
+ except OSError:
110
+ return None
111
+
112
+
113
+ def _run_git(path: Path, args: list[str]) -> subprocess.CompletedProcess[str] | None:
114
+ try:
115
+ return subprocess.run(
116
+ ["git", "-C", os.fspath(path), *args],
117
+ check=False,
118
+ capture_output=True,
119
+ text=True,
120
+ env=_git_probe_env(),
121
+ timeout=_GIT_PROBE_TIMEOUT_S,
122
+ )
123
+ except (OSError, subprocess.TimeoutExpired):
124
+ return None
125
+
126
+
127
+ def _git_probe_env() -> dict[str, str]:
128
+ return build_git_process_env()
129
+
130
+
131
+ def _git_resolution_from_root(workspace_root: Path) -> _GitResolution:
132
+ current_branch = _git_current_branch(workspace_root)
133
+ has_head_commit = _git_has_head_commit(workspace_root)
134
+ if current_branch is None and not has_head_commit:
135
+ current_branch, has_head_commit = _filesystem_head_state(workspace_root)
136
+ return _GitResolution(
137
+ workspace_root=workspace_root,
138
+ git_root=workspace_root,
139
+ has_head_commit=has_head_commit,
140
+ current_branch=current_branch,
141
+ )
142
+
143
+
144
+ def _git_current_branch(workspace_root: Path) -> str | None:
145
+ symbolic = _run_git(workspace_root, ["symbolic-ref", "--quiet", "--short", "HEAD"])
146
+ if symbolic is not None and symbolic.returncode == 0:
147
+ branch = (symbolic.stdout or "").strip()
148
+ if branch:
149
+ return branch
150
+
151
+ cp = _run_git(workspace_root, ["rev-parse", "--abbrev-ref", "HEAD"])
152
+ if cp is None or cp.returncode != 0:
153
+ return None
154
+ branch = (cp.stdout or "").strip()
155
+ if not branch or branch == "HEAD":
156
+ return None
157
+ return branch
158
+
159
+
160
+ def _git_has_head_commit(workspace_root: Path) -> bool:
161
+ cp = _run_git(workspace_root, ["rev-parse", "--verify", "HEAD"])
162
+ return cp is not None and cp.returncode == 0
163
+
164
+
165
+ def _find_git_root_from_filesystem(path: Path) -> Path | None:
166
+ for candidate in (path, *path.parents):
167
+ marker = candidate / ".git"
168
+ if marker.exists() and _is_valid_filesystem_git_root(candidate):
169
+ return candidate
170
+ return None
171
+
172
+
173
+ def _is_valid_filesystem_git_root(workspace_root: Path) -> bool:
174
+ git_dir = _resolve_git_dir(workspace_root)
175
+ if git_dir is None:
176
+ return False
177
+ return (git_dir / "HEAD").is_file()
178
+
179
+
180
+ def _filesystem_head_state(workspace_root: Path) -> tuple[str | None, bool]:
181
+ git_dir = _resolve_git_dir(workspace_root)
182
+ if git_dir is None:
183
+ return None, False
184
+ head_path = git_dir / "HEAD"
185
+ try:
186
+ head = head_path.read_text(encoding="utf-8").strip()
187
+ except OSError:
188
+ return None, False
189
+ if not head:
190
+ return None, False
191
+ if head.startswith(_HEAD_REF_PREFIX):
192
+ ref_name = head[len(_HEAD_REF_PREFIX) :].strip()
193
+ branch = _branch_name_from_ref(ref_name)
194
+ return branch, _git_ref_exists(git_dir, ref_name)
195
+ return None, bool(_OID_RE.fullmatch(head))
196
+
197
+
198
+ def _resolve_git_dir(workspace_root: Path) -> Path | None:
199
+ marker = workspace_root / ".git"
200
+ if marker.is_dir():
201
+ return marker.resolve()
202
+ if not marker.is_file():
203
+ return None
204
+ try:
205
+ first_line = marker.read_text(encoding="utf-8").splitlines()[0].strip()
206
+ except (IndexError, OSError, UnicodeDecodeError):
207
+ return None
208
+ if not first_line.lower().startswith("gitdir:"):
209
+ return None
210
+ value = first_line.split(":", 1)[1].strip()
211
+ candidate = Path(value)
212
+ if not candidate.is_absolute():
213
+ candidate = workspace_root / candidate
214
+ try:
215
+ return candidate.resolve()
216
+ except OSError:
217
+ return None
218
+
219
+
220
+ def _git_ref_exists(git_dir: Path, ref_name: str) -> bool:
221
+ for base_dir in _git_state_dirs(git_dir):
222
+ ref_path = base_dir / ref_name
223
+ try:
224
+ value = ref_path.read_text(encoding="utf-8").strip()
225
+ except OSError:
226
+ value = ""
227
+ if _OID_RE.fullmatch(value):
228
+ return True
229
+
230
+ packed_refs = base_dir / "packed-refs"
231
+ try:
232
+ for line in packed_refs.read_text(encoding="utf-8").splitlines():
233
+ entry = line.strip()
234
+ if not entry or entry.startswith("#") or entry.startswith("^"):
235
+ continue
236
+ oid, sep, packed_ref = entry.partition(" ")
237
+ if sep and packed_ref == ref_name and _OID_RE.fullmatch(oid):
238
+ return True
239
+ except OSError:
240
+ continue
241
+ return False
242
+
243
+
244
+ def _git_state_dirs(git_dir: Path) -> list[Path]:
245
+ dirs = [git_dir]
246
+ commondir_path = git_dir / "commondir"
247
+ try:
248
+ commondir_value = commondir_path.read_text(encoding="utf-8").strip()
249
+ except OSError:
250
+ commondir_value = ""
251
+ if not commondir_value:
252
+ return dirs
253
+
254
+ candidate = Path(commondir_value)
255
+ if not candidate.is_absolute():
256
+ candidate = git_dir / candidate
257
+ try:
258
+ resolved = candidate.resolve()
259
+ except OSError:
260
+ return dirs
261
+ if resolved not in dirs:
262
+ dirs.append(resolved)
263
+ return dirs
264
+
265
+
266
+ def _branch_name_from_ref(ref_name: str) -> str | None:
267
+ ref_name = ref_name.strip()
268
+ if not ref_name:
269
+ return None
270
+ if ref_name.startswith(_HEADS_PREFIX):
271
+ branch = ref_name[len(_HEADS_PREFIX) :].strip()
272
+ return branch or None
273
+ return ref_name