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,445 @@
1
+ """The lifecycle state of a workspace's current Forge run.
2
+
3
+ ``.alysis/current_run.json`` used to be a pure pointer: which run directory is
4
+ current, and how the workspace was bound. That left every surface guessing at the
5
+ one thing users actually ask -- *is this run going, finished, or dead?* -- and a
6
+ crashed process left behind a pointer indistinguishable from a healthy one, so
7
+ UIs showed a phantom "active" run forever.
8
+
9
+ This module owns the answer. The pointer now carries an explicit ``status`` drawn
10
+ from a closed enum:
11
+
12
+ ``draft``
13
+ A run exists and its plan is not execution-ready yet.
14
+ ``approved``
15
+ The plan passes the same acceptance gate ``forge exec`` applies. Executable.
16
+ ``running``
17
+ A process is executing the plan right now, and says which process.
18
+ ``interrupted``
19
+ Execution stopped without reaching a terminal state. Resumable.
20
+ ``completed`` / ``failed``
21
+ Terminal. Execution finished; the work was, or was not, accepted.
22
+
23
+ ``interrupted`` is the state that makes crash recovery honest: nothing writes it
24
+ during a normal run, so it can only be reached by
25
+ :func:`reconcile_status_after_crash` observing ``running`` with no live lock.
26
+
27
+ The module is pure -- dict in, dict out, no filesystem and no imports from
28
+ :mod:`alysis_code.forge` -- so both the pointer writer and the lock layer
29
+ can depend on it without a cycle.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import hashlib
35
+ import json
36
+ from collections.abc import Mapping, Sequence
37
+ from dataclasses import dataclass
38
+ from datetime import UTC, datetime
39
+ from typing import Any
40
+
41
+ RUN_STATE_SCHEMA_VERSION = 1
42
+
43
+ RUN_STATUS_DRAFT = "draft"
44
+ RUN_STATUS_APPROVED = "approved"
45
+ RUN_STATUS_RUNNING = "running"
46
+ RUN_STATUS_INTERRUPTED = "interrupted"
47
+ RUN_STATUS_COMPLETED = "completed"
48
+ RUN_STATUS_FAILED = "failed"
49
+
50
+ RUN_STATUSES: tuple[str, ...] = (
51
+ RUN_STATUS_DRAFT,
52
+ RUN_STATUS_APPROVED,
53
+ RUN_STATUS_RUNNING,
54
+ RUN_STATUS_INTERRUPTED,
55
+ RUN_STATUS_COMPLETED,
56
+ RUN_STATUS_FAILED,
57
+ )
58
+
59
+ # Statuses whose work is unfinished and can be picked back up by `forge resume`.
60
+ # ``failed`` is included on purpose: a run that stopped on a rejected task is
61
+ # exactly the run a retry flag exists for.
62
+ RESUMABLE_RUN_STATUSES: frozenset[str] = frozenset({RUN_STATUS_INTERRUPTED, RUN_STATUS_FAILED})
63
+ TERMINAL_RUN_STATUSES: frozenset[str] = frozenset({RUN_STATUS_COMPLETED, RUN_STATUS_FAILED})
64
+
65
+ STATUS_KEY = "status"
66
+ STATUS_UPDATED_AT_KEY = "status_updated_at"
67
+ STATUS_REASON_KEY = "status_reason"
68
+ STATUS_HISTORY_KEY = "status_history"
69
+ RUN_OWNER_KEY = "run_owner"
70
+ PLAN_FINGERPRINT_KEY = "plan_fingerprint"
71
+
72
+ # Bounded so a workspace that starts and stops a run a thousand times does not grow
73
+ # an unbounded pointer file. The tail is what diagnoses a crash; the head is noise.
74
+ STATUS_HISTORY_LIMIT = 24
75
+
76
+ PLAN_FINGERPRINT_SCHEMA_VERSION = 1
77
+
78
+ # The plan's *authored definition*. Deliberately excludes everything execution is
79
+ # allowed to move on its own -- ``status``, ``attempts``, ``branch`` -- so a run's
80
+ # own progress can never read as someone having edited the plan underneath it.
81
+ _TASK_DEFINITION_KEYS: tuple[str, ...] = (
82
+ "title",
83
+ "description",
84
+ "acceptance_criteria",
85
+ "dependencies",
86
+ "estimated_files",
87
+ "write_scope",
88
+ "task_kind",
89
+ "mcp_scope",
90
+ )
91
+
92
+ _TASK_FIELD_LABELS: dict[str, str] = {
93
+ "title": "title",
94
+ "description": "description",
95
+ "acceptance_criteria": "acceptance criteria",
96
+ "dependencies": "dependencies",
97
+ "estimated_files": "estimated files",
98
+ "write_scope": "write scope",
99
+ "task_kind": "task kind",
100
+ "mcp_scope": "MCP scope",
101
+ }
102
+
103
+
104
+ def _now_iso() -> str:
105
+ return datetime.now(UTC).replace(microsecond=0).isoformat()
106
+
107
+
108
+ def normalize_run_status(value: Any) -> str | None:
109
+ """Return the enum member ``value`` names, or ``None`` if it names none."""
110
+ text = str(value or "").strip().lower()
111
+ return text if text in RUN_STATUSES else None
112
+
113
+
114
+ def pointer_status(pointer: Mapping[str, Any] | None) -> str:
115
+ """The recorded status, defaulting to ``draft``.
116
+
117
+ Pointers written before this schema carry no status at all. ``draft`` is the
118
+ honest default for them: it is the only value that claims nothing about work
119
+ this build never observed.
120
+ """
121
+ if pointer is None:
122
+ return RUN_STATUS_DRAFT
123
+ return normalize_run_status(pointer.get(STATUS_KEY)) or RUN_STATUS_DRAFT
124
+
125
+
126
+ def status_is_resumable(status: str) -> bool:
127
+ return (normalize_run_status(status) or "") in RESUMABLE_RUN_STATUSES
128
+
129
+
130
+ def status_is_terminal(status: str) -> bool:
131
+ return (normalize_run_status(status) or "") in TERMINAL_RUN_STATUSES
132
+
133
+
134
+ def describe_run_status(status: str) -> str:
135
+ normalized = normalize_run_status(status) or RUN_STATUS_DRAFT
136
+ return {
137
+ RUN_STATUS_DRAFT: "plan is not execution-ready yet",
138
+ RUN_STATUS_APPROVED: "plan is execution-ready and not started",
139
+ RUN_STATUS_RUNNING: "a process is executing this plan",
140
+ RUN_STATUS_INTERRUPTED: "execution stopped without finishing",
141
+ RUN_STATUS_COMPLETED: "execution finished and the work was accepted",
142
+ RUN_STATUS_FAILED: "execution finished and the work was not accepted",
143
+ }[normalized]
144
+
145
+
146
+ def build_run_owner(
147
+ *,
148
+ pid: int,
149
+ hostname: str,
150
+ mode: str,
151
+ started_at: str | None = None,
152
+ session_id: str | None = None,
153
+ ) -> dict[str, Any]:
154
+ """The identity a ``running`` pointer publishes about its executing process."""
155
+ return {
156
+ "pid": int(pid),
157
+ "hostname": str(hostname),
158
+ "mode": str(mode),
159
+ "started_at": started_at or _now_iso(),
160
+ "session_id": str(session_id) if session_id else None,
161
+ }
162
+
163
+
164
+ def apply_run_status(
165
+ pointer: Mapping[str, Any] | None,
166
+ status: str,
167
+ *,
168
+ reason: str = "",
169
+ owner: Mapping[str, Any] | None = None,
170
+ plan_fingerprint: Mapping[str, Any] | None = None,
171
+ at: str | None = None,
172
+ ) -> dict[str, Any]:
173
+ """Return ``pointer`` with ``status`` applied and the transition recorded.
174
+
175
+ Pure: the caller decides whether the result is worth writing. ``owner`` is
176
+ cleared on any status other than ``running`` -- a stale owner block on a
177
+ finished run is the same phantom-active problem in a different field.
178
+ """
179
+ normalized = normalize_run_status(status)
180
+ if normalized is None:
181
+ raise ValueError(
182
+ f"unknown run status {status!r}; expected one of {', '.join(RUN_STATUSES)}"
183
+ )
184
+ stamp = at or _now_iso()
185
+ updated: dict[str, Any] = dict(pointer or {})
186
+ previous = pointer_status(pointer) if pointer else None
187
+
188
+ updated[STATUS_KEY] = normalized
189
+ updated[STATUS_UPDATED_AT_KEY] = stamp
190
+ if reason:
191
+ updated[STATUS_REASON_KEY] = reason
192
+ else:
193
+ updated.pop(STATUS_REASON_KEY, None)
194
+
195
+ if normalized == RUN_STATUS_RUNNING:
196
+ if owner is not None:
197
+ updated[RUN_OWNER_KEY] = dict(owner)
198
+ else:
199
+ updated.pop(RUN_OWNER_KEY, None)
200
+
201
+ if plan_fingerprint is not None:
202
+ updated[PLAN_FINGERPRINT_KEY] = dict(plan_fingerprint)
203
+
204
+ history = list(updated.get(STATUS_HISTORY_KEY) or [])
205
+ entry: dict[str, Any] = {"status": normalized, "at": stamp}
206
+ if previous and previous != normalized:
207
+ entry["from"] = previous
208
+ if reason:
209
+ entry["reason"] = reason
210
+ history.append(entry)
211
+ updated[STATUS_HISTORY_KEY] = history[-STATUS_HISTORY_LIMIT:]
212
+ return updated
213
+
214
+
215
+ def reconcile_status_after_crash(
216
+ pointer: Mapping[str, Any] | None,
217
+ *,
218
+ lock_is_live: bool,
219
+ at: str | None = None,
220
+ ) -> dict[str, Any] | None:
221
+ """Transition a crash leftover to ``interrupted``, or return ``None``.
222
+
223
+ ``running`` with no live lock is the crash signature: the executing process
224
+ holds the workspace lock for its whole run and releases it on the way out, so a
225
+ pointer claiming ``running`` while nothing holds the lock describes a process
226
+ that is not there. Every other combination is left exactly as it is -- this
227
+ function's only job is to stop UIs from showing a run that nobody is running.
228
+ """
229
+ if pointer is None:
230
+ return None
231
+ if pointer_status(pointer) != RUN_STATUS_RUNNING:
232
+ return None
233
+ if lock_is_live:
234
+ return None
235
+ owner = pointer.get(RUN_OWNER_KEY)
236
+ detail = ""
237
+ if isinstance(owner, Mapping):
238
+ pid = owner.get("pid")
239
+ host = owner.get("hostname")
240
+ if pid and host:
241
+ detail = f" (owner pid {pid} on {host} is gone)"
242
+ return apply_run_status(
243
+ pointer,
244
+ RUN_STATUS_INTERRUPTED,
245
+ reason=f"run was marked running but no execution holds the workspace lock{detail}",
246
+ at=at,
247
+ )
248
+
249
+
250
+ # ---------------------------------------------------------------------------
251
+ # Plan fingerprints
252
+ # ---------------------------------------------------------------------------
253
+
254
+
255
+ def _digest(payload: Any) -> str:
256
+ canonical = json.dumps(payload, sort_keys=True, ensure_ascii=False, separators=(",", ":"))
257
+ return hashlib.sha256(canonical.encode("utf-8")).hexdigest()[:32]
258
+
259
+
260
+ def _task_id(task: Mapping[str, Any]) -> str:
261
+ return str(task.get("id") or "").strip()
262
+
263
+
264
+ def _plan_tasks(plan: Mapping[str, Any]) -> list[Mapping[str, Any]]:
265
+ tasks = plan.get("tasks")
266
+ if not isinstance(tasks, Sequence):
267
+ return []
268
+ return [task for task in tasks if isinstance(task, Mapping) and _task_id(task)]
269
+
270
+
271
+ def _requirement_texts(plan: Mapping[str, Any]) -> list[str]:
272
+ raw = plan.get("requirements")
273
+ if not isinstance(raw, Sequence):
274
+ return []
275
+ texts: list[str] = []
276
+ for item in raw:
277
+ if isinstance(item, Mapping):
278
+ for key in ("text", "requirement", "title", "description", "content"):
279
+ value = item.get(key)
280
+ if isinstance(value, str) and value.strip():
281
+ texts.append(value.strip())
282
+ break
283
+ elif str(item).strip():
284
+ texts.append(str(item).strip())
285
+ return texts
286
+
287
+
288
+ def plan_fingerprint(plan: Mapping[str, Any]) -> dict[str, Any]:
289
+ """A comparable digest of everything about a plan that a human authored.
290
+
291
+ Field-level rather than whole-file, so drift can be reported as *what* changed
292
+ ("T02 acceptance criteria changed") instead of the useless "the plan changed".
293
+ """
294
+ task_digests = {
295
+ _task_id(task): {
296
+ key: _digest(task.get(key)) for key in _TASK_DEFINITION_KEYS if key in task
297
+ }
298
+ for task in _plan_tasks(plan)
299
+ }
300
+ payload = {
301
+ "schema_version": PLAN_FINGERPRINT_SCHEMA_VERSION,
302
+ "project_goal": _digest(str(plan.get("project_goal") or "").strip()),
303
+ "summary": _digest(str(plan.get("summary") or "").strip()),
304
+ "requirements": _digest(_requirement_texts(plan)),
305
+ "task_order": [_task_id(task) for task in _plan_tasks(plan)],
306
+ "tasks": task_digests,
307
+ }
308
+ payload["digest"] = _digest(
309
+ {key: value for key, value in payload.items() if key != "schema_version"}
310
+ )
311
+ return payload
312
+
313
+
314
+ @dataclass(frozen=True)
315
+ class PlanDrift:
316
+ """What changed between the plan a run was approved against and today's plan."""
317
+
318
+ changed: bool
319
+ goal_changed: bool = False
320
+ summary_changed: bool = False
321
+ requirements_changed: bool = False
322
+ order_changed: bool = False
323
+ tasks_added: tuple[str, ...] = ()
324
+ tasks_removed: tuple[str, ...] = ()
325
+ tasks_changed: tuple[tuple[str, tuple[str, ...]], ...] = ()
326
+ comparable: bool = True
327
+
328
+ @property
329
+ def reasons(self) -> tuple[str, ...]:
330
+ """One human line per distinct change, for the re-approval prompt."""
331
+ if not self.comparable:
332
+ return ("this run has no recorded plan fingerprint to compare against",)
333
+ lines: list[str] = []
334
+ if self.goal_changed:
335
+ lines.append("project goal changed")
336
+ if self.summary_changed:
337
+ lines.append("plan summary changed")
338
+ if self.requirements_changed:
339
+ lines.append("requirements changed")
340
+ if self.tasks_added:
341
+ lines.append(f"tasks added: {', '.join(self.tasks_added)}")
342
+ if self.tasks_removed:
343
+ lines.append(f"tasks removed: {', '.join(self.tasks_removed)}")
344
+ for task_id, fields in self.tasks_changed:
345
+ labels = ", ".join(_TASK_FIELD_LABELS.get(name, name) for name in fields)
346
+ lines.append(f"{task_id} changed: {labels}")
347
+ if self.order_changed and not (self.tasks_added or self.tasks_removed):
348
+ lines.append("task order changed")
349
+ return tuple(lines)
350
+
351
+ def to_json(self) -> dict[str, Any]:
352
+ return {
353
+ "changed": self.changed,
354
+ "comparable": self.comparable,
355
+ "goal_changed": self.goal_changed,
356
+ "summary_changed": self.summary_changed,
357
+ "requirements_changed": self.requirements_changed,
358
+ "order_changed": self.order_changed,
359
+ "tasks_added": list(self.tasks_added),
360
+ "tasks_removed": list(self.tasks_removed),
361
+ "tasks_changed": [
362
+ {"task_id": task_id, "fields": list(fields)}
363
+ for task_id, fields in self.tasks_changed
364
+ ],
365
+ "reasons": list(self.reasons),
366
+ }
367
+
368
+
369
+ def compare_plan_fingerprints(
370
+ recorded: Mapping[str, Any] | None,
371
+ current: Mapping[str, Any],
372
+ ) -> PlanDrift:
373
+ """Diff a stored fingerprint against a freshly computed one.
374
+
375
+ A missing or unreadable stored fingerprint is reported as *not comparable*
376
+ rather than as "no drift": a run whose baseline was never recorded has not been
377
+ shown to be unchanged, and resume must not claim otherwise.
378
+ """
379
+ if not isinstance(recorded, Mapping) or not recorded.get("digest"):
380
+ return PlanDrift(changed=True, comparable=False)
381
+ if recorded.get("digest") == current.get("digest"):
382
+ return PlanDrift(changed=False)
383
+
384
+ recorded_tasks = recorded.get("tasks")
385
+ current_tasks = current.get("tasks")
386
+ recorded_tasks = recorded_tasks if isinstance(recorded_tasks, Mapping) else {}
387
+ current_tasks = current_tasks if isinstance(current_tasks, Mapping) else {}
388
+
389
+ added = tuple(sorted(set(current_tasks) - set(recorded_tasks)))
390
+ removed = tuple(sorted(set(recorded_tasks) - set(current_tasks)))
391
+
392
+ changed: list[tuple[str, tuple[str, ...]]] = []
393
+ for task_id in sorted(set(recorded_tasks) & set(current_tasks)):
394
+ before = recorded_tasks.get(task_id)
395
+ after = current_tasks.get(task_id)
396
+ if not isinstance(before, Mapping) or not isinstance(after, Mapping):
397
+ continue
398
+ fields = tuple(
399
+ sorted(key for key in set(before) | set(after) if before.get(key) != after.get(key))
400
+ )
401
+ if fields:
402
+ changed.append((task_id, fields))
403
+
404
+ recorded_order = list(recorded.get("task_order") or [])
405
+ current_order = list(current.get("task_order") or [])
406
+ return PlanDrift(
407
+ changed=True,
408
+ goal_changed=recorded.get("project_goal") != current.get("project_goal"),
409
+ summary_changed=recorded.get("summary") != current.get("summary"),
410
+ requirements_changed=recorded.get("requirements") != current.get("requirements"),
411
+ order_changed=recorded_order != current_order,
412
+ tasks_added=added,
413
+ tasks_removed=removed,
414
+ tasks_changed=tuple(changed),
415
+ )
416
+
417
+
418
+ __all__ = [
419
+ "PLAN_FINGERPRINT_KEY",
420
+ "PLAN_FINGERPRINT_SCHEMA_VERSION",
421
+ "RESUMABLE_RUN_STATUSES",
422
+ "RUN_OWNER_KEY",
423
+ "RUN_STATE_SCHEMA_VERSION",
424
+ "RUN_STATUSES",
425
+ "RUN_STATUS_APPROVED",
426
+ "RUN_STATUS_COMPLETED",
427
+ "RUN_STATUS_DRAFT",
428
+ "RUN_STATUS_FAILED",
429
+ "RUN_STATUS_INTERRUPTED",
430
+ "RUN_STATUS_RUNNING",
431
+ "STATUS_HISTORY_KEY",
432
+ "STATUS_KEY",
433
+ "TERMINAL_RUN_STATUSES",
434
+ "PlanDrift",
435
+ "apply_run_status",
436
+ "build_run_owner",
437
+ "compare_plan_fingerprints",
438
+ "describe_run_status",
439
+ "normalize_run_status",
440
+ "plan_fingerprint",
441
+ "pointer_status",
442
+ "reconcile_status_after_crash",
443
+ "status_is_resumable",
444
+ "status_is_terminal",
445
+ ]
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path, PurePosixPath
5
+
6
+ ROOT_RUNTIME_ARTIFACT_DIR_NAMES = frozenset(
7
+ {".alysis", ".alysis_images", ".git", "alysis-feedback"}
8
+ )
9
+ RUNTIME_CACHE_DIR_NAMES = frozenset(
10
+ {
11
+ "__pycache__",
12
+ ".mypy_cache",
13
+ ".pytest_cache",
14
+ ".ruff_cache",
15
+ }
16
+ )
17
+ RUST_RUNTIME_ARTIFACT_DIR_NAME = "target"
18
+ RUST_MANIFEST_FILENAME = "Cargo.toml"
19
+ RUNTIME_ARTIFACT_FILE_NAMES = frozenset({".coverage"})
20
+ RUNTIME_ARTIFACT_FILE_SUFFIXES = frozenset({".pyc", ".pyo"})
21
+ RUNTIME_ARTIFACT_DIR_NAMES = ROOT_RUNTIME_ARTIFACT_DIR_NAMES | RUNTIME_CACHE_DIR_NAMES
22
+ RUNTIME_ARTIFACT_GIT_EXCLUDE_ENTRIES = (
23
+ "/.alysis/",
24
+ "/.alysis_images/",
25
+ "/alysis-feedback/",
26
+ "__pycache__/",
27
+ ".mypy_cache/",
28
+ ".pytest_cache/",
29
+ ".ruff_cache/",
30
+ ".coverage",
31
+ "*.pyc",
32
+ "*.pyo",
33
+ )
34
+
35
+
36
+ def normalize_runtime_artifact_path(value: str) -> str:
37
+ cleaned = str(value).strip().replace("\\", "/")
38
+ while cleaned.startswith("./"):
39
+ cleaned = cleaned[2:]
40
+ return cleaned
41
+
42
+
43
+ def _is_rust_target_runtime_artifact(
44
+ *,
45
+ parts: tuple[str, ...],
46
+ root: Path | None,
47
+ ) -> bool:
48
+ if not parts:
49
+ return False
50
+ if root is None:
51
+ return False
52
+
53
+ root_resolved = root.resolve()
54
+ if parts[0] == RUST_RUNTIME_ARTIFACT_DIR_NAME:
55
+ return (root_resolved / RUST_MANIFEST_FILENAME).is_file()
56
+ for index, segment in enumerate(parts[:-1]):
57
+ if segment != RUST_RUNTIME_ARTIFACT_DIR_NAME:
58
+ continue
59
+ target_parent = root_resolved.joinpath(*parts[:index])
60
+ if (target_parent / RUST_MANIFEST_FILENAME).is_file():
61
+ return True
62
+ return False
63
+
64
+
65
+ def _iter_rust_manifest_dirs(root: Path) -> tuple[Path, ...]:
66
+ root_resolved = root.resolve()
67
+ manifest_dirs: set[Path] = set()
68
+ for current_root, dirnames, filenames in os.walk(root_resolved):
69
+ dirnames[:] = [
70
+ dirname
71
+ for dirname in dirnames
72
+ if dirname not in ROOT_RUNTIME_ARTIFACT_DIR_NAMES
73
+ and dirname not in RUNTIME_CACHE_DIR_NAMES
74
+ and dirname != RUST_RUNTIME_ARTIFACT_DIR_NAME
75
+ ]
76
+ if RUST_MANIFEST_FILENAME not in filenames:
77
+ continue
78
+ manifest_dirs.add(Path(current_root))
79
+ return tuple(sorted(manifest_dirs))
80
+
81
+
82
+ def runtime_artifact_git_exclude_entries(root: Path) -> tuple[str, ...]:
83
+ root_resolved = root.resolve()
84
+ rust_target_entries: list[str] = []
85
+ for manifest_dir in _iter_rust_manifest_dirs(root_resolved):
86
+ relative_dir = manifest_dir.relative_to(root_resolved).as_posix()
87
+ if relative_dir in {"", "."}:
88
+ rust_target_entries.append("/target/")
89
+ else:
90
+ rust_target_entries.append(f"/{relative_dir}/target/")
91
+ return (*RUNTIME_ARTIFACT_GIT_EXCLUDE_ENTRIES, *rust_target_entries)
92
+
93
+
94
+ def has_grounded_rust_target_runtime_artifacts(root: Path) -> bool:
95
+ return bool(_iter_rust_manifest_dirs(root))
96
+
97
+
98
+ def is_runtime_artifact_path(path: str, *, root: Path | None = None) -> bool:
99
+ cleaned = normalize_runtime_artifact_path(path)
100
+ if not cleaned:
101
+ return False
102
+
103
+ parts = tuple(segment for segment in PurePosixPath(cleaned).parts if segment not in {"", "."})
104
+ if not parts:
105
+ return False
106
+ if parts[0] in ROOT_RUNTIME_ARTIFACT_DIR_NAMES:
107
+ return True
108
+ if any(segment in RUNTIME_CACHE_DIR_NAMES for segment in parts):
109
+ return True
110
+ if _is_rust_target_runtime_artifact(parts=parts, root=root):
111
+ return True
112
+
113
+ filename = parts[-1]
114
+ if filename in RUNTIME_ARTIFACT_FILE_NAMES:
115
+ return True
116
+ return any(filename.endswith(suffix) for suffix in RUNTIME_ARTIFACT_FILE_SUFFIXES)
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from .compaction.settings import CompactionSettings
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class RuntimeContextFeatures:
10
+ requested_enable_compaction: bool
11
+ requested_tool_output_offload: bool
12
+ requested_conversation_summarization: bool
13
+ tool_output_offload_enabled: bool
14
+ conversation_summarization_enabled: bool
15
+ logging_enabled: bool
16
+ explicit_session_artifact_root: bool
17
+ tool_output_offload_artifact_persistence_available: bool
18
+ settings_enabled: bool
19
+ settings_offload_tool_outputs: bool
20
+ settings_summarize_conversation: bool
21
+
22
+ @property
23
+ def any_enabled(self) -> bool:
24
+ return self.tool_output_offload_enabled or self.conversation_summarization_enabled
25
+
26
+
27
+ def resolve_runtime_context_features(
28
+ *,
29
+ settings: CompactionSettings,
30
+ enable_compaction: bool,
31
+ enable_tool_output_offload: bool | None = None,
32
+ enable_conversation_summarization: bool | None = None,
33
+ logging_enabled: bool = True,
34
+ explicit_session_artifact_root: bool = False,
35
+ ) -> RuntimeContextFeatures:
36
+ requested_enable_compaction = bool(enable_compaction)
37
+ requested_tool_output_offload = (
38
+ requested_enable_compaction
39
+ if enable_tool_output_offload is None
40
+ else bool(enable_tool_output_offload)
41
+ )
42
+ requested_conversation_summarization = (
43
+ requested_enable_compaction
44
+ if enable_conversation_summarization is None
45
+ else bool(enable_conversation_summarization)
46
+ )
47
+
48
+ settings_enabled = bool(settings.enabled)
49
+ settings_offload_tool_outputs = bool(settings.offload_tool_outputs)
50
+ settings_summarize_conversation = bool(settings.summarize_conversation)
51
+ tool_output_offload_artifact_persistence_available = bool(
52
+ logging_enabled or explicit_session_artifact_root
53
+ )
54
+
55
+ return RuntimeContextFeatures(
56
+ requested_enable_compaction=requested_enable_compaction,
57
+ requested_tool_output_offload=requested_tool_output_offload,
58
+ requested_conversation_summarization=requested_conversation_summarization,
59
+ tool_output_offload_enabled=(
60
+ requested_tool_output_offload
61
+ and settings_enabled
62
+ and settings_offload_tool_outputs
63
+ and tool_output_offload_artifact_persistence_available
64
+ ),
65
+ conversation_summarization_enabled=(
66
+ requested_conversation_summarization
67
+ and settings_enabled
68
+ and settings_summarize_conversation
69
+ ),
70
+ logging_enabled=bool(logging_enabled),
71
+ explicit_session_artifact_root=bool(explicit_session_artifact_root),
72
+ tool_output_offload_artifact_persistence_available=(
73
+ tool_output_offload_artifact_persistence_available
74
+ ),
75
+ settings_enabled=settings_enabled,
76
+ settings_offload_tool_outputs=settings_offload_tool_outputs,
77
+ settings_summarize_conversation=settings_summarize_conversation,
78
+ )
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import StrEnum
4
+
5
+ from .config import ConfigError
6
+
7
+
8
+ class RuntimeKind(StrEnum):
9
+ INTERACTIVE_CHAT = "interactive_chat"
10
+ ONE_SHOT = "one_shot"
11
+ FORGE_EXEC = "forge_exec"
12
+ SWARM_WORKER = "swarm_worker"
13
+ SUBAGENT = "subagent"
14
+ CONFLICT_AUTO_RESOLVE = "conflict_auto_resolve"
15
+
16
+ def __str__(self) -> str:
17
+ return self.value
18
+
19
+
20
+ def runtime_kind_values() -> tuple[str, ...]:
21
+ return tuple(kind.value for kind in RuntimeKind)
22
+
23
+
24
+ def normalize_runtime_kind(
25
+ value: RuntimeKind | str | None,
26
+ *,
27
+ fallback: RuntimeKind | None = None,
28
+ ) -> RuntimeKind:
29
+ if isinstance(value, RuntimeKind):
30
+ return value
31
+ if value is None:
32
+ if fallback is None:
33
+ raise ConfigError("Missing runtime kind.")
34
+ return fallback
35
+ normalized = str(value).strip().lower()
36
+ for kind in RuntimeKind:
37
+ if kind.value == normalized:
38
+ return kind
39
+ allowed = ", ".join(runtime_kind_values())
40
+ raise ConfigError(f"Invalid runtime kind: {value!r}. Expected one of: {allowed}")
41
+
42
+
43
+ def resolve_session_runtime_kind(
44
+ *,
45
+ runtime_kind: RuntimeKind | str | None,
46
+ one_shot_execution: bool,
47
+ subagent_depth: int,
48
+ ) -> RuntimeKind:
49
+ fallback = RuntimeKind.ONE_SHOT if one_shot_execution else RuntimeKind.INTERACTIVE_CHAT
50
+ if subagent_depth > 0:
51
+ fallback = RuntimeKind.SUBAGENT
52
+ return normalize_runtime_kind(runtime_kind, fallback=fallback)