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,657 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import platform
5
+ import queue
6
+ import subprocess
7
+ import sys
8
+ import threading
9
+ from collections.abc import Mapping
10
+ from dataclasses import dataclass
11
+ from datetime import UTC, datetime
12
+ from pathlib import Path
13
+ from typing import Protocol
14
+ from uuid import uuid4
15
+
16
+ from ..atomic_io import atomic_write_json
17
+ from ..branding import default_sandbox_docker_image, env_get
18
+ from ..bwrap_etc import ensure_minimal_etc_dir
19
+ from ..forge_events import EVENT_RUN_COMPLETED, is_terminal_event, parse_event_line
20
+ from ..logging_redaction import redact_log_text
21
+ from .settings import ServerSettings
22
+ from .store import JobPaths, RunPaths, ServerStore, ServerStoreError
23
+
24
+ _DEFAULT_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
25
+ _TERMINAL_JOB_STATUSES = {"succeeded", "failed", "cancelled"}
26
+
27
+ # How a job's terminal status was decided. "terminal_event" means the worker told us in
28
+ # its own words; "exit_code" means we had to infer it from the process result.
29
+ STATUS_SOURCE_TERMINAL_EVENT = "terminal_event"
30
+ STATUS_SOURCE_EXIT_CODE = "exit_code"
31
+
32
+
33
+ class WorkerRunnerError(RuntimeError):
34
+ pass
35
+
36
+
37
+ def _now_iso() -> str:
38
+ return datetime.now(UTC).replace(microsecond=0).isoformat()
39
+
40
+
41
+ def _make_job_id() -> str:
42
+ ts = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
43
+ return f"job_{ts}_{uuid4().hex[:8]}"
44
+
45
+
46
+ def _supports_bwrap_unshare_cgroup() -> bool:
47
+ try:
48
+ cp = subprocess.run(
49
+ ["bwrap", "--help"],
50
+ check=False,
51
+ capture_output=True,
52
+ text=True,
53
+ )
54
+ except OSError:
55
+ return False
56
+ help_text = (cp.stdout or "") + (cp.stderr or "")
57
+ return cp.returncode == 0 and "--unshare-cgroup" in help_text
58
+
59
+
60
+ def _runtime_roots() -> list[Path]:
61
+ candidates: list[Path] = []
62
+ for raw in [sys.prefix, sys.base_prefix, os.path.dirname(sys.executable)]:
63
+ if not raw:
64
+ continue
65
+ p = Path(raw).resolve()
66
+ if p.exists():
67
+ candidates.append(p)
68
+ for raw in sys.path:
69
+ if not raw or not os.path.isabs(raw):
70
+ continue
71
+ p = Path(raw).resolve()
72
+ if p.exists():
73
+ candidates.append(p)
74
+
75
+ unique: list[Path] = []
76
+ seen: set[Path] = set()
77
+ for candidate in sorted(candidates, key=lambda p: len(p.parts)):
78
+ if candidate in seen:
79
+ continue
80
+ if any(candidate == parent or candidate.is_relative_to(parent) for parent in unique):
81
+ continue
82
+ unique.append(candidate)
83
+ seen.add(candidate)
84
+ return unique
85
+
86
+
87
+ class OuterProcessRunner(Protocol):
88
+ def spawn(
89
+ self,
90
+ *,
91
+ workspace: Path,
92
+ job_dir: Path,
93
+ argv: list[str],
94
+ env: dict[str, str],
95
+ ) -> subprocess.Popen[str]: ...
96
+
97
+
98
+ @dataclass(frozen=True)
99
+ class BwrapProcessRunner:
100
+ network: str = "on"
101
+
102
+ def build_argv(
103
+ self,
104
+ *,
105
+ workspace: Path,
106
+ job_dir: Path,
107
+ argv: list[str],
108
+ env: dict[str, str],
109
+ ) -> list[str]:
110
+ workspace_abs = workspace.resolve()
111
+ job_dir_abs = job_dir.resolve()
112
+ args: list[str] = [
113
+ "bwrap",
114
+ "--die-with-parent",
115
+ "--new-session",
116
+ "--clearenv",
117
+ "--unshare-pid",
118
+ "--unshare-ipc",
119
+ "--unshare-uts",
120
+ "--bind",
121
+ os.fspath(workspace_abs),
122
+ "/workspace",
123
+ "--dir",
124
+ "/alysis_job",
125
+ "--bind",
126
+ os.fspath(job_dir_abs),
127
+ "/alysis_job",
128
+ ]
129
+ if _supports_bwrap_unshare_cgroup():
130
+ args.append("--unshare-cgroup")
131
+ if self.network == "off":
132
+ args.append("--unshare-net")
133
+
134
+ for sys_dir in ("/usr", "/bin", "/lib", "/lib64"):
135
+ if Path(sys_dir).exists():
136
+ args.extend(["--ro-bind", sys_dir, sys_dir])
137
+
138
+ etc_dir = ensure_minimal_etc_dir(network=self.network)
139
+ args.extend(
140
+ [
141
+ "--dir",
142
+ "/etc",
143
+ "--ro-bind",
144
+ os.fspath(etc_dir / "passwd"),
145
+ "/etc/passwd",
146
+ "--ro-bind",
147
+ os.fspath(etc_dir / "group"),
148
+ "/etc/group",
149
+ "--ro-bind",
150
+ os.fspath(etc_dir / "nsswitch.conf"),
151
+ "/etc/nsswitch.conf",
152
+ "--ro-bind",
153
+ os.fspath(etc_dir / "hosts"),
154
+ "/etc/hosts",
155
+ "--ro-bind",
156
+ os.fspath(etc_dir / "resolv.conf"),
157
+ "/etc/resolv.conf",
158
+ "--dir",
159
+ "/etc/ssl",
160
+ "--dir",
161
+ "/etc/ssl/certs",
162
+ ]
163
+ )
164
+ if Path("/etc/ssl/certs").exists():
165
+ args.extend(["--ro-bind", "/etc/ssl/certs", "/etc/ssl/certs"])
166
+ args.extend(["--dir", "/etc/pki", "--dir", "/etc/pki/tls", "--dir", "/etc/pki/tls/certs"])
167
+ if Path("/etc/pki/tls/certs").exists():
168
+ args.extend(["--ro-bind", "/etc/pki/tls/certs", "/etc/pki/tls/certs"])
169
+ args.extend(["--dir", "/etc/ca-certificates"])
170
+ if Path("/etc/ca-certificates").exists():
171
+ args.extend(["--ro-bind", "/etc/ca-certificates", "/etc/ca-certificates"])
172
+
173
+ for root in _runtime_roots():
174
+ if root == workspace_abs or root.is_relative_to(workspace_abs):
175
+ continue
176
+ if root == job_dir_abs or root.is_relative_to(job_dir_abs):
177
+ continue
178
+ args.extend(["--ro-bind", os.fspath(root), os.fspath(root)])
179
+
180
+ args.extend(
181
+ [
182
+ "--proc",
183
+ "/proc",
184
+ "--dev",
185
+ "/dev",
186
+ "--tmpfs",
187
+ "/tmp",
188
+ "--dir",
189
+ "/tmp/home",
190
+ "--chdir",
191
+ "/workspace",
192
+ ]
193
+ )
194
+ for key, value in sorted(env.items()):
195
+ args.extend(["--setenv", key, value])
196
+ args.extend(argv)
197
+ return args
198
+
199
+ def spawn(
200
+ self,
201
+ *,
202
+ workspace: Path,
203
+ job_dir: Path,
204
+ argv: list[str],
205
+ env: dict[str, str],
206
+ ) -> subprocess.Popen[str]:
207
+ if platform.system().lower() != "linux":
208
+ raise WorkerRunnerError("bwrap worker backend requires Linux.")
209
+ if not shutil_which("bwrap"):
210
+ raise WorkerRunnerError(
211
+ "bwrap worker backend requested but bubblewrap is not available."
212
+ )
213
+ workspace_abs = workspace.resolve()
214
+ args = self.build_argv(workspace=workspace, job_dir=job_dir, argv=argv, env=env)
215
+ return subprocess.Popen(
216
+ args,
217
+ stdout=subprocess.PIPE,
218
+ stderr=subprocess.STDOUT,
219
+ text=True,
220
+ cwd=os.fspath(workspace_abs),
221
+ bufsize=1,
222
+ start_new_session=True,
223
+ )
224
+
225
+
226
+ @dataclass(frozen=True)
227
+ class DockerProcessRunner:
228
+ image: str = default_sandbox_docker_image("server")
229
+ network: str = "on"
230
+
231
+ def build_argv(
232
+ self,
233
+ *,
234
+ workspace: Path,
235
+ job_dir: Path,
236
+ argv: list[str],
237
+ env: dict[str, str],
238
+ ) -> list[str]:
239
+ workspace_abs = workspace.resolve()
240
+ job_dir_abs = job_dir.resolve()
241
+ args: list[str] = [
242
+ "docker",
243
+ "run",
244
+ "--rm",
245
+ "--cap-drop=ALL",
246
+ "--security-opt",
247
+ "no-new-privileges",
248
+ "--read-only",
249
+ "--tmpfs",
250
+ "/tmp:rw,exec,nosuid,nodev",
251
+ "-v",
252
+ f"{os.fspath(workspace_abs)}:/workspace:rw",
253
+ "-v",
254
+ f"{os.fspath(job_dir_abs)}:/alysis_job:rw",
255
+ "-w",
256
+ "/workspace",
257
+ ]
258
+ if self.network == "off":
259
+ args.extend(["--network", "none"])
260
+ if os.name == "posix" and hasattr(os, "getuid") and hasattr(os, "getgid"):
261
+ args.extend(["--user", f"{os.getuid()}:{os.getgid()}"])
262
+ for key, value in sorted(env.items()):
263
+ args.extend(["-e", f"{key}={value}"])
264
+ args.append(self.image)
265
+ args.extend(argv)
266
+ return args
267
+
268
+ def spawn(
269
+ self,
270
+ *,
271
+ workspace: Path,
272
+ job_dir: Path,
273
+ argv: list[str],
274
+ env: dict[str, str],
275
+ ) -> subprocess.Popen[str]:
276
+ if not shutil_which("docker"):
277
+ raise WorkerRunnerError("docker worker backend requested but docker is not available.")
278
+ workspace_abs = workspace.resolve()
279
+ args = self.build_argv(workspace=workspace, job_dir=job_dir, argv=argv, env=env)
280
+ return subprocess.Popen(
281
+ args,
282
+ stdout=subprocess.PIPE,
283
+ stderr=subprocess.STDOUT,
284
+ text=True,
285
+ cwd=os.fspath(workspace_abs),
286
+ bufsize=1,
287
+ start_new_session=True,
288
+ )
289
+
290
+
291
+ def shutil_which(binary: str) -> str | None:
292
+ import shutil
293
+
294
+ return shutil.which(binary)
295
+
296
+
297
+ @dataclass
298
+ class JobState:
299
+ job_id: str
300
+ run_id: str
301
+ status: str
302
+ command: list[str]
303
+ created_at: str
304
+ started_at: str | None = None
305
+ finished_at: str | None = None
306
+ exit_code: int | None = None
307
+ error: str | None = None
308
+ logs_path: str | None = None
309
+ status_source: str | None = None
310
+ terminal_event: dict[str, object] | None = None
311
+ _cancel_requested: bool = False
312
+ _process: subprocess.Popen[str] | None = None
313
+
314
+
315
+ @dataclass(frozen=True)
316
+ class JobWorkItem:
317
+ job_id: str
318
+ run_paths: RunPaths
319
+ job_paths: JobPaths
320
+
321
+
322
+ @dataclass(frozen=True)
323
+ class JobStatus:
324
+ job_id: str
325
+ run_id: str
326
+ status: str
327
+ created_at: str
328
+ started_at: str | None
329
+ finished_at: str | None
330
+ exit_code: int | None
331
+ error: str | None
332
+ status_source: str | None = None
333
+ terminal_event: dict[str, object] | None = None
334
+
335
+
336
+ def job_status_from_terminal_event(event: Mapping[str, object] | None) -> str | None:
337
+ """Map a terminal Forge event to a job status, or None when it says nothing.
338
+
339
+ ``run_completed`` reports whether the command's work was accepted; ``error`` means
340
+ the command itself failed. Everything else is not terminal and decides nothing.
341
+ """
342
+ if not isinstance(event, Mapping) or not is_terminal_event(event):
343
+ return None
344
+ if str(event.get("event")) != EVENT_RUN_COMPLETED:
345
+ return "failed"
346
+ data = event.get("data")
347
+ ok = data.get("ok") if isinstance(data, Mapping) else None
348
+ if isinstance(ok, bool):
349
+ return "succeeded" if ok else "failed"
350
+ # A run_completed without an explicit verdict is not evidence of success.
351
+ return None
352
+
353
+
354
+ def _terminal_event_error(event: Mapping[str, object] | None) -> str | None:
355
+ """Surface the worker's own error message when it reported one."""
356
+ if not isinstance(event, Mapping) or str(event.get("event")) == EVENT_RUN_COMPLETED:
357
+ return None
358
+ data = event.get("data")
359
+ if not isinstance(data, Mapping):
360
+ return None
361
+ message = data.get("message")
362
+ return str(message) if message else None
363
+
364
+
365
+ class JobRunner:
366
+ def __init__(self, settings: ServerSettings, store: ServerStore) -> None:
367
+ self._settings = settings
368
+ self._store = store
369
+ self._jobs: dict[str, JobState] = {}
370
+ self._job_paths: dict[str, JobPaths] = {}
371
+ self._jobs_lock = threading.Lock()
372
+ # Fixed-size worker pool: jobs queue here until a worker claims them.
373
+ self._job_queue: queue.Queue[JobWorkItem | None] = queue.Queue()
374
+ self._worker_threads: list[threading.Thread] = []
375
+ self._closed = False
376
+ self._docker_image = env_get(
377
+ "ALYSIS_SERVER_DOCKER_IMAGE",
378
+ default=env_get(
379
+ "ALYSIS_SHELL_SANDBOX_DOCKER_IMAGE",
380
+ default=default_sandbox_docker_image("server"),
381
+ ),
382
+ )
383
+ self._start_workers()
384
+
385
+ def _start_workers(self) -> None:
386
+ for idx in range(self._settings.max_concurrent_jobs):
387
+ thread = threading.Thread(
388
+ target=self._worker_loop,
389
+ name=f"alysis-server-worker-{idx + 1}",
390
+ daemon=True,
391
+ )
392
+ thread.start()
393
+ self._worker_threads.append(thread)
394
+
395
+ def close(self) -> None:
396
+ with self._jobs_lock:
397
+ if self._closed:
398
+ return
399
+ self._closed = True
400
+ for _ in self._worker_threads:
401
+ self._job_queue.put(None)
402
+ for thread in self._worker_threads:
403
+ thread.join(timeout=1.0)
404
+
405
+ def start_job(self, *, run_id: str, command: list[str]) -> str:
406
+ run_paths = self._store.get_run_paths(run_id)
407
+ job_id = _make_job_id()
408
+ job_paths = self._store.create_job_paths(run_id, job_id)
409
+ state = JobState(
410
+ job_id=job_id,
411
+ run_id=run_id,
412
+ status="queued",
413
+ command=command,
414
+ created_at=_now_iso(),
415
+ logs_path=os.fspath(job_paths.logs_path),
416
+ )
417
+ with self._jobs_lock:
418
+ if self._closed:
419
+ raise WorkerRunnerError("Job runner is closed.")
420
+ self._jobs[job_id] = state
421
+ self._job_paths[job_id] = job_paths
422
+ self._write_meta(job_paths, state)
423
+ self._job_queue.put(JobWorkItem(job_id=job_id, run_paths=run_paths, job_paths=job_paths))
424
+ return job_id
425
+
426
+ def cancel_job(self, job_id: str) -> None:
427
+ persist_terminal = False
428
+ proc: subprocess.Popen[str] | None = None
429
+ with self._jobs_lock:
430
+ state = self._jobs.get(job_id)
431
+ if state is None:
432
+ raise ServerStoreError(f"Job not found: {job_id}")
433
+ if state.status in _TERMINAL_JOB_STATUSES:
434
+ return
435
+ state._cancel_requested = True
436
+ if state.status == "queued":
437
+ state.status = "cancelled"
438
+ state.finished_at = _now_iso()
439
+ state._process = None
440
+ persist_terminal = True
441
+ else:
442
+ proc = state._process
443
+ job_paths = self._job_paths.get(job_id)
444
+ if proc is not None and proc.poll() is None:
445
+ try:
446
+ proc.terminate()
447
+ except OSError:
448
+ pass
449
+ if persist_terminal and job_paths is not None:
450
+ self._write_meta(job_paths, state)
451
+ self._write_result(job_paths, state, error=None)
452
+
453
+ def get_status(self, job_id: str) -> JobStatus:
454
+ with self._jobs_lock:
455
+ state = self._jobs.get(job_id)
456
+ if state is None:
457
+ raise ServerStoreError(f"Job not found: {job_id}")
458
+ status = state.status
459
+ job_paths = self._job_paths.get(job_id)
460
+ if (
461
+ status in _TERMINAL_JOB_STATUSES
462
+ and job_paths is not None
463
+ and not job_paths.result_path.exists()
464
+ ):
465
+ status = "running" if state.started_at is not None else "queued"
466
+ return JobStatus(
467
+ job_id=state.job_id,
468
+ run_id=state.run_id,
469
+ status=status,
470
+ created_at=state.created_at,
471
+ started_at=state.started_at,
472
+ finished_at=state.finished_at,
473
+ exit_code=state.exit_code,
474
+ error=state.error,
475
+ status_source=state.status_source,
476
+ terminal_event=state.terminal_event,
477
+ )
478
+
479
+ def read_logs(self, job_id: str) -> str:
480
+ with self._jobs_lock:
481
+ state = self._jobs.get(job_id)
482
+ if state is None:
483
+ raise ServerStoreError(f"Job not found: {job_id}")
484
+ logs_path = state.logs_path
485
+ if not logs_path:
486
+ raise ServerStoreError(f"Job has no logs path: {job_id}")
487
+ path = Path(logs_path)
488
+ if not path.exists():
489
+ return ""
490
+ return path.read_text(encoding="utf-8", errors="replace")
491
+
492
+ def _build_worker_env(self) -> dict[str, str]:
493
+ if self._settings.worker_backend == "docker":
494
+ path_value = _DEFAULT_PATH
495
+ else:
496
+ path_value = os.environ.get("PATH") or _DEFAULT_PATH
497
+ env: dict[str, str] = {
498
+ "PATH": path_value,
499
+ "HOME": "/tmp",
500
+ "GIT_CONFIG_NOSYSTEM": "1",
501
+ "GIT_CONFIG_GLOBAL": "/dev/null",
502
+ "GIT_TERMINAL_PROMPT": "0",
503
+ "PYTHONUNBUFFERED": "1",
504
+ "ALYSIS_SHELL_SANDBOX_MODE": self._settings.worker_sandbox_mode,
505
+ "ALYSIS_SHELL_SANDBOX_BACKEND": "bwrap",
506
+ "ALYSIS_SHELL_SANDBOX_NETWORK": "off",
507
+ "ALYSIS_SHELL_SANDBOX_CLEAR_ENV": "1",
508
+ "ALYSIS_SHELL_SANDBOX_BWRAP_PROFILE": "hardened",
509
+ "ALYSIS_SHELL_SANDBOX_PROTECT_REPO_META": "1",
510
+ "ALYSIS_VERIFY_SANDBOX_MODE": self._settings.worker_sandbox_mode,
511
+ }
512
+ lang = os.environ.get("LANG")
513
+ if lang:
514
+ env["LANG"] = lang
515
+ api_key = env_get("ALYSIS_API_KEY") or os.environ.get("OPENAI_API_KEY")
516
+ if api_key:
517
+ env["ALYSIS_API_KEY"] = api_key
518
+ return env
519
+
520
+ def _outer_runner(self) -> OuterProcessRunner:
521
+ if self._settings.worker_backend == "bwrap":
522
+ return BwrapProcessRunner(network=self._settings.worker_network)
523
+ return DockerProcessRunner(image=self._docker_image, network=self._settings.worker_network)
524
+
525
+ def _worker_loop(self) -> None:
526
+ while True:
527
+ item = self._job_queue.get()
528
+ try:
529
+ if item is None:
530
+ return
531
+ with self._jobs_lock:
532
+ state = self._jobs.get(item.job_id)
533
+ if state is None:
534
+ continue
535
+ # Workers skip jobs that were cancelled while they were waiting in the queue.
536
+ self._run_job_inner(state, item.run_paths, item.job_paths)
537
+ finally:
538
+ self._job_queue.task_done()
539
+
540
+ def _run_job_inner(self, state: JobState, run_paths: RunPaths, job_paths: JobPaths) -> None:
541
+ with self._jobs_lock:
542
+ if state.status != "queued":
543
+ return
544
+ state.status = "running"
545
+ state.started_at = _now_iso()
546
+ self._write_meta(job_paths, state)
547
+
548
+ worker_env = self._build_worker_env()
549
+ (job_paths.job_dir / "config").mkdir(parents=True, exist_ok=True)
550
+ (job_paths.job_dir / "data").mkdir(parents=True, exist_ok=True)
551
+ worker_env["ALYSIS_CONFIG_DIR"] = "/alysis_job/config"
552
+ worker_env["ALYSIS_DATA_DIR"] = "/alysis_job/data"
553
+
554
+ runner = self._outer_runner()
555
+ exit_code: int | None = None
556
+ error: str | None = None
557
+ terminal_event: dict[str, object] | None = None
558
+ try:
559
+ proc = runner.spawn(
560
+ workspace=run_paths.workspace_dir,
561
+ job_dir=job_paths.job_dir,
562
+ argv=state.command,
563
+ env=worker_env,
564
+ )
565
+ cancel_requested = False
566
+ with self._jobs_lock:
567
+ state._process = proc
568
+ cancel_requested = state._cancel_requested
569
+ if cancel_requested and proc.poll() is None:
570
+ try:
571
+ proc.terminate()
572
+ except OSError:
573
+ pass
574
+ with job_paths.logs_path.open("a", encoding="utf-8") as logs:
575
+ logs.write(redact_log_text(f"[{_now_iso()}] starting job {state.job_id}\n"))
576
+ if proc.stdout is not None:
577
+ for line in proc.stdout:
578
+ # Only the on-disk copy is redacted; `line` stays verbatim
579
+ # below so terminal-event parsing is unaffected.
580
+ logs.write(redact_log_text(line))
581
+ logs.flush()
582
+ # Machine-mode workers narrate themselves. Keep the last
583
+ # terminal event: it, not the exit code, is what the job
584
+ # actually reported.
585
+ event = parse_event_line(line)
586
+ if event is not None and is_terminal_event(event):
587
+ terminal_event = event
588
+ exit_code = proc.wait()
589
+ if state._cancel_requested:
590
+ with self._jobs_lock:
591
+ state.status = "cancelled"
592
+ state.finished_at = _now_iso()
593
+ state.exit_code = exit_code
594
+ state.status_source = STATUS_SOURCE_EXIT_CODE
595
+ state.terminal_event = terminal_event
596
+ state._process = None
597
+ else:
598
+ reported_status = job_status_from_terminal_event(terminal_event)
599
+ reported_error = _terminal_event_error(terminal_event)
600
+ with self._jobs_lock:
601
+ if reported_status is not None:
602
+ state.status = reported_status
603
+ state.status_source = STATUS_SOURCE_TERMINAL_EVENT
604
+ if reported_error:
605
+ state.error = reported_error
606
+ else:
607
+ state.status = "succeeded" if exit_code == 0 else "failed"
608
+ state.status_source = STATUS_SOURCE_EXIT_CODE
609
+ state.finished_at = _now_iso()
610
+ state.exit_code = exit_code
611
+ state.terminal_event = terminal_event
612
+ state._process = None
613
+ except Exception as e: # noqa: BLE001
614
+ error = str(e)
615
+ with self._jobs_lock:
616
+ state.status = "failed"
617
+ state.finished_at = _now_iso()
618
+ state.error = error
619
+ state.exit_code = 1 if exit_code is None else exit_code
620
+ state.status_source = STATUS_SOURCE_EXIT_CODE
621
+ state.terminal_event = terminal_event
622
+ state._process = None
623
+ with job_paths.logs_path.open("a", encoding="utf-8") as logs:
624
+ logs.write(redact_log_text(f"[{_now_iso()}] job failed to start: {error}\n"))
625
+
626
+ self._write_meta(job_paths, state)
627
+ self._write_result(job_paths, state, error=error)
628
+
629
+ def _write_meta(self, job_paths: JobPaths, state: JobState) -> None:
630
+ payload = {
631
+ "job_id": state.job_id,
632
+ "run_id": state.run_id,
633
+ "status": state.status,
634
+ "status_source": state.status_source,
635
+ "command": state.command,
636
+ "created_at": state.created_at,
637
+ "started_at": state.started_at,
638
+ "finished_at": state.finished_at,
639
+ "exit_code": state.exit_code,
640
+ "error": state.error,
641
+ "logs_path": state.logs_path,
642
+ }
643
+ atomic_write_json(job_paths.meta_path, payload)
644
+
645
+ def _write_result(self, job_paths: JobPaths, state: JobState, *, error: str | None) -> None:
646
+ payload = {
647
+ "job_id": state.job_id,
648
+ "run_id": state.run_id,
649
+ "status": state.status,
650
+ "status_source": state.status_source,
651
+ "exit_code": state.exit_code,
652
+ "error": error or state.error,
653
+ "started_at": state.started_at,
654
+ "finished_at": state.finished_at,
655
+ "terminal_event": state.terminal_event,
656
+ }
657
+ atomic_write_json(job_paths.result_path, payload)