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,465 @@
1
+ """Non-fakeable build identity: which commit is actually running.
2
+
3
+ Three behaviourally different builds once all reported themselves as version
4
+ ``0.9.8``, and one benchmark campaign turned out to have run against an
5
+ unpinned "latest main". Every score those runs produced is therefore
6
+ unattributable: there is no way, now, to say which source tree earned which
7
+ number. A version string is a promise the developer makes; a commit hash
8
+ recorded at build time is a fact about the artifact.
9
+
10
+ This module owns three things:
11
+
12
+ *The generated fact.* ``scripts/generate_build_info.py`` writes
13
+ ``_build_info.py`` next to this file, stamping the commit, an ISO-8601 UTC
14
+ build timestamp, and whether the working tree carried uncommitted changes. The
15
+ copy committed to the repository is a deliberate *dev default* -- no commit,
16
+ marked dirty -- so that an artifact built without running the generator is
17
+ correctly reported as unidentifiable rather than quietly inheriting a stale
18
+ hash.
19
+
20
+ *The reading.* :func:`load_build_info` tolerates the file being absent,
21
+ truncated or hand-edited, because the failure mode that matters is a build
22
+ that cannot prove its identity, and that must surface as a clear refusal
23
+ rather than an import error.
24
+
25
+ *The refusal.* :func:`decide_clean_build` is the policy behind
26
+ ``--require-clean-build`` / ``ALYSIS_REQUIRE_CLEAN_BUILD``. Benchmark
27
+ harnesses set it so a campaign cannot start against a build whose provenance
28
+ is unknown -- the precise mistake that cost the earlier campaign its meaning.
29
+
30
+ Stdlib only, no package imports: the generator script runs this in a bare
31
+ interpreter before the package is installable, and the tests load it straight
32
+ from this file path.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import importlib.util
38
+ import json
39
+ import os
40
+ import re
41
+ import subprocess
42
+ from collections.abc import Callable, Mapping, Sequence
43
+ from dataclasses import dataclass
44
+ from datetime import datetime, timezone
45
+ from pathlib import Path
46
+ from typing import Any
47
+
48
+ BUILD_INFO_FILENAME = "_build_info.py"
49
+ BUILD_INFO_SCHEMA_VERSION = 1
50
+
51
+ REQUIRE_CLEAN_BUILD_ENV = "ALYSIS_REQUIRE_CLEAN_BUILD"
52
+
53
+ #: Written into the committed dev default. Any artifact still reporting this
54
+ #: source was built without the release generator.
55
+ DEV_DEFAULT_SOURCE = "dev-default"
56
+ GENERATED_SOURCE = "git"
57
+
58
+ #: The generator rewrites this file immediately before the wheel is built, so
59
+ #: git necessarily sees it as modified. Counting that self-inflicted edit as a
60
+ #: dirty tree would make every release build report itself dirty, so the one
61
+ #: path the generator writes is excluded from the dirty computation.
62
+ DIRTY_CHECK_EXCLUDED_PATHS = ("src/alysis_code/" + BUILD_INFO_FILENAME,)
63
+
64
+ _TRUTHY = frozenset({"1", "true", "yes", "on", "enabled", "require"})
65
+ _COMMIT_RE = re.compile(r"\A[0-9a-f]{7,64}\Z")
66
+ _TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
67
+
68
+ _BUILD_INFO_ATTRIBUTES = (
69
+ "BUILD_COMMIT",
70
+ "BUILD_TIMESTAMP",
71
+ "BUILD_DIRTY",
72
+ "BUILD_SOURCE",
73
+ "BUILD_INFO_SCHEMA_VERSION",
74
+ )
75
+
76
+ UNKNOWN = "unknown"
77
+
78
+
79
+ # ---------------------------------------------------------------------------
80
+ # The record
81
+ # ---------------------------------------------------------------------------
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class BuildInfo:
86
+ """What the artifact can prove about its own provenance."""
87
+
88
+ commit: str = ""
89
+ timestamp: str = ""
90
+ dirty: bool = True
91
+ source: str = DEV_DEFAULT_SOURCE
92
+ schema_version: int = BUILD_INFO_SCHEMA_VERSION
93
+
94
+ @property
95
+ def commit_short(self) -> str:
96
+ return self.commit[:12] if self.commit else ""
97
+
98
+ @property
99
+ def is_identifiable(self) -> bool:
100
+ """True when the recorded commit is a plausible git object name.
101
+
102
+ Shape is checked rather than trusted: a hand-edited ``_build_info.py``
103
+ containing ``BUILD_COMMIT = "probably main"`` must not pass for
104
+ provenance.
105
+ """
106
+ return bool(_COMMIT_RE.fullmatch(self.commit.strip().casefold()))
107
+
108
+ @property
109
+ def is_clean(self) -> bool:
110
+ return self.is_identifiable and not self.dirty
111
+
112
+ def describe(self) -> str:
113
+ """One-line human summary, as printed by ``alysis --version``."""
114
+ return (
115
+ f"commit: {self.commit_short or UNKNOWN}, "
116
+ f"built: {self.timestamp or UNKNOWN}, "
117
+ f"dirty: {'yes' if self.dirty else 'no'}, "
118
+ f"source: {self.source or UNKNOWN}"
119
+ )
120
+
121
+ def telemetry_payload(self) -> dict[str, Any]:
122
+ return {
123
+ "schema_version": self.schema_version,
124
+ "commit": self.commit,
125
+ "commit_short": self.commit_short,
126
+ "timestamp": self.timestamp,
127
+ "dirty": self.dirty,
128
+ "source": self.source,
129
+ "identifiable": self.is_identifiable,
130
+ "clean": self.is_clean,
131
+ }
132
+
133
+
134
+ DEV_DEFAULT_BUILD_INFO = BuildInfo()
135
+
136
+
137
+ def version_line(version: str, info: BuildInfo | None = None) -> str:
138
+ """Render the ``--version`` line.
139
+
140
+ The bare version stays the leading token and the whole thing stays on one
141
+ line: the VS Code extension, the managed-CLI smoke test and the release
142
+ distribution validator all read this output, and they read it by prefix,
143
+ substring and non-emptiness respectively.
144
+ """
145
+ resolved = load_build_info() if info is None else info
146
+ return f"{str(version).strip()} ({resolved.describe()})"
147
+
148
+
149
+ # ---------------------------------------------------------------------------
150
+ # Reading the generated file
151
+ # ---------------------------------------------------------------------------
152
+
153
+
154
+ def _coerce_build_info(namespace: Mapping[str, Any]) -> BuildInfo:
155
+ commit = str(namespace.get("BUILD_COMMIT") or "").strip()
156
+ timestamp = str(namespace.get("BUILD_TIMESTAMP") or "").strip()
157
+ source = str(namespace.get("BUILD_SOURCE") or "").strip() or DEV_DEFAULT_SOURCE
158
+ raw_dirty = namespace.get("BUILD_DIRTY", True)
159
+ # Anything unparseable resolves to dirty. Provenance failures must fail
160
+ # closed: a build that cannot say whether it was clean is not clean.
161
+ if isinstance(raw_dirty, bool):
162
+ dirty = raw_dirty
163
+ elif isinstance(raw_dirty, (int, float)):
164
+ dirty = bool(raw_dirty)
165
+ elif isinstance(raw_dirty, str):
166
+ dirty = raw_dirty.strip().casefold() not in {"false", "0", "no", "off"}
167
+ else:
168
+ dirty = True
169
+ try:
170
+ schema_version = int(namespace.get("BUILD_INFO_SCHEMA_VERSION", BUILD_INFO_SCHEMA_VERSION))
171
+ except (TypeError, ValueError):
172
+ schema_version = BUILD_INFO_SCHEMA_VERSION
173
+ return BuildInfo(
174
+ commit=commit,
175
+ timestamp=timestamp,
176
+ dirty=dirty,
177
+ source=source,
178
+ schema_version=schema_version,
179
+ )
180
+
181
+
182
+ def _namespace_from_path(path: Path) -> dict[str, Any] | None:
183
+ try:
184
+ if not path.is_file():
185
+ return None
186
+ spec = importlib.util.spec_from_file_location("_alysis_build_info", path)
187
+ if spec is None or spec.loader is None:
188
+ return None
189
+ module = importlib.util.module_from_spec(spec)
190
+ spec.loader.exec_module(module)
191
+ except Exception: # noqa: BLE001 - a corrupt stamp must not break startup
192
+ return None
193
+ return {name: getattr(module, name) for name in _BUILD_INFO_ATTRIBUTES if hasattr(module, name)}
194
+
195
+
196
+ def default_build_info_path() -> Path:
197
+ return Path(__file__).resolve().parent / BUILD_INFO_FILENAME
198
+
199
+
200
+ def read_build_info(path: str | os.PathLike[str] | None = None) -> BuildInfo:
201
+ """Read build info from disk, uncached.
202
+
203
+ Prefers the installed package module so a frozen or zipped distribution
204
+ resolves the same way the rest of the package does, and falls back to
205
+ loading the sibling file by path -- which is also what makes this readable
206
+ from a bare interpreter that has never imported the package.
207
+ """
208
+ if path is None:
209
+ try:
210
+ from . import _build_info as module # noqa: PLC0415
211
+ except ImportError:
212
+ module = None
213
+ except Exception: # noqa: BLE001 - corrupt stamp, fall through to path load
214
+ module = None
215
+ if module is not None:
216
+ namespace = {
217
+ name: getattr(module, name)
218
+ for name in _BUILD_INFO_ATTRIBUTES
219
+ if hasattr(module, name)
220
+ }
221
+ if namespace:
222
+ return _coerce_build_info(namespace)
223
+ path = default_build_info_path()
224
+ namespace = _namespace_from_path(Path(path))
225
+ if namespace is None:
226
+ return DEV_DEFAULT_BUILD_INFO
227
+ return _coerce_build_info(namespace)
228
+
229
+
230
+ _CACHED_BUILD_INFO: BuildInfo | None = None
231
+
232
+
233
+ def load_build_info() -> BuildInfo:
234
+ """Read build info once per process."""
235
+ global _CACHED_BUILD_INFO
236
+ if _CACHED_BUILD_INFO is None:
237
+ _CACHED_BUILD_INFO = read_build_info()
238
+ return _CACHED_BUILD_INFO
239
+
240
+
241
+ def reset_build_info_cache_for_tests() -> None:
242
+ global _CACHED_BUILD_INFO
243
+ _CACHED_BUILD_INFO = None
244
+
245
+
246
+ # ---------------------------------------------------------------------------
247
+ # Generating the file
248
+ # ---------------------------------------------------------------------------
249
+
250
+ GitRunner = Callable[[Sequence[str]], "tuple[int, str]"]
251
+
252
+
253
+ def _run_git(args: Sequence[str], *, cwd: Path) -> tuple[int, str]:
254
+ try:
255
+ completed = subprocess.run( # noqa: S603 - fixed argv, no shell
256
+ ["git", *args],
257
+ cwd=str(cwd),
258
+ capture_output=True,
259
+ text=True,
260
+ check=False,
261
+ timeout=30,
262
+ )
263
+ except (OSError, subprocess.SubprocessError):
264
+ return 1, ""
265
+ return completed.returncode, completed.stdout
266
+
267
+
268
+ def build_timestamp(
269
+ now: datetime | None = None,
270
+ *,
271
+ environ: Mapping[str, str] | None = None,
272
+ ) -> str:
273
+ """ISO-8601 UTC build timestamp, honouring ``SOURCE_DATE_EPOCH``.
274
+
275
+ The reproducible-builds variable is respected so that a deliberately
276
+ reproducible release does not become irreproducible because of this stamp.
277
+ """
278
+ source: Mapping[str, str] = os.environ if environ is None else environ
279
+ raw_epoch = str(source.get("SOURCE_DATE_EPOCH") or "").strip()
280
+ if raw_epoch:
281
+ try:
282
+ return datetime.fromtimestamp(int(raw_epoch), tz=timezone.utc).strftime(
283
+ _TIMESTAMP_FORMAT
284
+ )
285
+ except (TypeError, ValueError, OSError, OverflowError):
286
+ pass
287
+ moment = datetime.now(timezone.utc) if now is None else now
288
+ if moment.tzinfo is None:
289
+ moment = moment.replace(tzinfo=timezone.utc)
290
+ return moment.astimezone(timezone.utc).strftime(_TIMESTAMP_FORMAT)
291
+
292
+
293
+ def parse_dirty_status(porcelain_output: str) -> bool:
294
+ """Decide dirtiness from ``git status --porcelain`` output.
295
+
296
+ The generator's own output file is excluded (see
297
+ :data:`DIRTY_CHECK_EXCLUDED_PATHS`); everything else that git reports --
298
+ modified, staged, or untracked-and-not-ignored -- counts, because any of
299
+ them can change what the built artifact actually executes.
300
+ """
301
+ for line in str(porcelain_output or "").splitlines():
302
+ entry = line.strip()
303
+ if not entry:
304
+ continue
305
+ # Porcelain v1: two status characters, a space, then the path. A rename
306
+ # reads "R old -> new"; the destination is what ships.
307
+ path = entry[2:].strip() if len(entry) > 2 else ""
308
+ if "->" in path:
309
+ path = path.split("->")[-1].strip()
310
+ path = path.strip('"')
311
+ if path and path not in DIRTY_CHECK_EXCLUDED_PATHS:
312
+ return True
313
+ return False
314
+
315
+
316
+ def generate_build_info(
317
+ *,
318
+ repo_root: str | os.PathLike[str],
319
+ now: datetime | None = None,
320
+ git_runner: GitRunner | None = None,
321
+ environ: Mapping[str, str] | None = None,
322
+ ) -> BuildInfo:
323
+ """Interrogate git and return the build stamp for the current tree.
324
+
325
+ A tree with no git available, or no repository, yields the dev default:
326
+ unidentifiable and dirty. That is the honest answer, and it is the answer
327
+ ``--require-clean-build`` is designed to reject.
328
+ """
329
+ root = Path(repo_root)
330
+ runner: GitRunner = git_runner if git_runner is not None else (lambda a: _run_git(a, cwd=root))
331
+
332
+ code, out = runner(["rev-parse", "HEAD"])
333
+ commit = out.strip().splitlines()[0].strip().casefold() if code == 0 and out.strip() else ""
334
+ if not _COMMIT_RE.fullmatch(commit):
335
+ return BuildInfo(
336
+ commit="",
337
+ timestamp=build_timestamp(now, environ=environ),
338
+ dirty=True,
339
+ source=DEV_DEFAULT_SOURCE,
340
+ )
341
+
342
+ status_code, status_out = runner(["status", "--porcelain"])
343
+ # A failed status probe is treated as dirty for the same fail-closed reason
344
+ # as an unparseable stamp: not knowing is not the same as being clean.
345
+ dirty = True if status_code != 0 else parse_dirty_status(status_out)
346
+ return BuildInfo(
347
+ commit=commit,
348
+ timestamp=build_timestamp(now, environ=environ),
349
+ dirty=dirty,
350
+ source=GENERATED_SOURCE,
351
+ )
352
+
353
+
354
+ def render_build_info_module(info: BuildInfo) -> str:
355
+ """Render the generated ``_build_info.py`` source text.
356
+
357
+ String literals are emitted via ``json.dumps`` rather than ``repr`` so the
358
+ generated file is double-quoted and survives the repository's
359
+ ``ruff format`` check unchanged.
360
+ """
361
+ return (
362
+ '"""Generated build identity. Do not edit by hand.\n'
363
+ "\n"
364
+ "Regenerate with ``python3 scripts/generate_build_info.py``. The copy\n"
365
+ "committed to the repository is the dev default and deliberately reports an\n"
366
+ "unidentifiable build, so that an artifact built without running the\n"
367
+ "generator is refused by ``--require-clean-build`` instead of inheriting a\n"
368
+ "stale commit hash.\n"
369
+ '"""\n'
370
+ "\n"
371
+ f"BUILD_COMMIT = {json.dumps(info.commit)}\n"
372
+ f"BUILD_TIMESTAMP = {json.dumps(info.timestamp)}\n"
373
+ f"BUILD_DIRTY = {bool(info.dirty)!r}\n"
374
+ f"BUILD_SOURCE = {json.dumps(info.source)}\n"
375
+ f"BUILD_INFO_SCHEMA_VERSION = {int(info.schema_version)!r}\n"
376
+ )
377
+
378
+
379
+ def write_build_info(info: BuildInfo, path: str | os.PathLike[str]) -> Path:
380
+ target = Path(path)
381
+ target.parent.mkdir(parents=True, exist_ok=True)
382
+ target.write_text(render_build_info_module(info), encoding="utf-8", newline="\n")
383
+ return target
384
+
385
+
386
+ # ---------------------------------------------------------------------------
387
+ # The refusal
388
+ # ---------------------------------------------------------------------------
389
+
390
+
391
+ @dataclass(frozen=True)
392
+ class CleanBuildDecision:
393
+ """Whether startup may proceed under ``--require-clean-build``."""
394
+
395
+ required: bool
396
+ allowed: bool
397
+ reason: str
398
+ message: str = ""
399
+
400
+ def payload(self) -> dict[str, Any]:
401
+ return {
402
+ "required": self.required,
403
+ "allowed": self.allowed,
404
+ "reason": self.reason,
405
+ }
406
+
407
+
408
+ def require_clean_build_requested(
409
+ *,
410
+ environ: Mapping[str, str] | None = None,
411
+ flag: bool | None = None,
412
+ ) -> bool:
413
+ """True when this run must refuse an unidentifiable build.
414
+
415
+ An explicit ``--require-clean-build`` wins over the environment; otherwise
416
+ the harness-set variable decides. An unrecognised value means "not
417
+ requested" rather than an error, so a typo cannot silently *enable* a
418
+ refusal that then looks like a build problem.
419
+ """
420
+ if flag:
421
+ return True
422
+ source: Mapping[str, str] = os.environ if environ is None else environ
423
+ raw = str(source.get(REQUIRE_CLEAN_BUILD_ENV) or "").strip().casefold()
424
+ return raw in _TRUTHY
425
+
426
+
427
+ def decide_clean_build(
428
+ *,
429
+ info: BuildInfo | None = None,
430
+ environ: Mapping[str, str] | None = None,
431
+ flag: bool | None = None,
432
+ ) -> CleanBuildDecision:
433
+ """Decide whether a run may start, and say why not in words an operator can act on."""
434
+ resolved = load_build_info() if info is None else info
435
+ required = require_clean_build_requested(environ=environ, flag=flag)
436
+ if not required:
437
+ return CleanBuildDecision(required=False, allowed=True, reason="not_required")
438
+ if not resolved.is_identifiable:
439
+ return CleanBuildDecision(
440
+ required=True,
441
+ allowed=False,
442
+ reason="missing_commit",
443
+ message=(
444
+ "This build cannot identify itself: no commit hash was recorded at "
445
+ f"build time (source: {resolved.source or UNKNOWN}). "
446
+ f"{REQUIRE_CLEAN_BUILD_ENV} is set, so the run is refused rather than "
447
+ "producing results that cannot be attributed to a source tree. "
448
+ "Rebuild after running scripts/generate_build_info.py, or unset "
449
+ f"{REQUIRE_CLEAN_BUILD_ENV} to proceed with an unidentifiable build."
450
+ ),
451
+ )
452
+ if resolved.dirty:
453
+ return CleanBuildDecision(
454
+ required=True,
455
+ allowed=False,
456
+ reason="dirty_tree",
457
+ message=(
458
+ f"This build was made from a dirty working tree at commit "
459
+ f"{resolved.commit_short} (built {resolved.timestamp or UNKNOWN}). "
460
+ f"{REQUIRE_CLEAN_BUILD_ENV} is set, so the run is refused: the commit "
461
+ "alone does not describe the code that would run. Commit or stash the "
462
+ f"changes and rebuild, or unset {REQUIRE_CLEAN_BUILD_ENV}."
463
+ ),
464
+ )
465
+ return CleanBuildDecision(required=True, allowed=True, reason="clean")
@@ -0,0 +1,7 @@
1
+ """Built-in hook handlers shipped with alysis-code.
2
+
3
+ Users can reference these modules from hooks.json with commands like
4
+ `python -m alysis_code.builtin_hooks.<name>`.
5
+ """
6
+
7
+ from __future__ import annotations
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env python3
2
+ """Windows-native lifecycle notification hook using winsound.
3
+
4
+ Built-in handler shipped with alysis-code; no path discovery needed.
5
+ Handles TurnComplete and Notification events.
6
+ TurnComplete with exit_code 0 plays MB_ICONASTERISK.
7
+ TurnComplete with any other exit_code plays MB_ICONHAND.
8
+ Notification with cause "pre_tool_use_blocked" plays MB_ICONEXCLAMATION.
9
+ Other Notification causes play MB_ICONQUESTION.
10
+ Other hook events exit silently.
11
+ Example .alysis/hooks.json:
12
+ {"hooks": {"TurnComplete": [{"hooks": [{"type": "command",
13
+ "command": "python -m alysis_code.builtin_hooks.notify_done_windows"}]}],
14
+ "Notification": [{"hooks": [{"type": "command",
15
+ "command": "python -m alysis_code.builtin_hooks.notify_done_windows"}]}]}}
16
+ The hook never writes stdout, so dispatcher JSON parsing stays clean.
17
+ On non-Windows or unavailable audio devices it exits 0 silently.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import sys
24
+
25
+ try:
26
+ import winsound
27
+ except ImportError:
28
+ winsound = None
29
+
30
+
31
+ def _read_payload() -> dict:
32
+ try:
33
+ payload = json.loads(sys.stdin.read() or "{}")
34
+ except (json.JSONDecodeError, UnicodeDecodeError):
35
+ return {}
36
+ return payload if isinstance(payload, dict) else {}
37
+
38
+
39
+ def _beep(sound: int) -> None:
40
+ if winsound is None:
41
+ return
42
+ try:
43
+ winsound.MessageBeep(sound)
44
+ except Exception: # noqa: BLE001 - notification hooks must never break a turn.
45
+ pass
46
+
47
+
48
+ def main() -> int:
49
+ if winsound is None:
50
+ return 0
51
+ payload = _read_payload()
52
+ event_name = payload.get("hook_event_name")
53
+ if event_name == "TurnComplete":
54
+ sound = winsound.MB_ICONASTERISK if payload.get("exit_code") == 0 else winsound.MB_ICONHAND
55
+ _beep(sound)
56
+ elif event_name == "Notification":
57
+ if payload.get("cause") == "pre_tool_use_blocked":
58
+ _beep(winsound.MB_ICONEXCLAMATION)
59
+ else:
60
+ _beep(winsound.MB_ICONQUESTION)
61
+ return 0
62
+
63
+
64
+ if __name__ == "__main__":
65
+ sys.exit(main())
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import tempfile
5
+ from pathlib import Path
6
+
7
+ _VALID_NETWORK = {"off", "on"}
8
+
9
+
10
+ def _write_if_changed(path: Path, content: str) -> None:
11
+ if path.exists():
12
+ try:
13
+ if path.read_text(encoding="utf-8") == content:
14
+ return
15
+ except OSError:
16
+ pass
17
+ path.write_text(content, encoding="utf-8")
18
+
19
+
20
+ def _resolve_uid_gid() -> tuple[int, int]:
21
+ uid = 0
22
+ gid = 0
23
+ if hasattr(os, "getuid"):
24
+ try:
25
+ uid = int(os.getuid())
26
+ except OSError:
27
+ uid = 0
28
+ if hasattr(os, "getgid"):
29
+ try:
30
+ gid = int(os.getgid())
31
+ except OSError:
32
+ gid = 0
33
+ return uid, gid
34
+
35
+
36
+ def ensure_minimal_etc_dir(*, network: str) -> Path:
37
+ mode = str(network).strip().lower()
38
+ if mode not in _VALID_NETWORK:
39
+ raise ValueError(f"Invalid network mode for minimal /etc: {network!r}")
40
+
41
+ root = Path(tempfile.gettempdir()) / "alysis-code" / "bwrap-etc" / mode
42
+ root.mkdir(parents=True, exist_ok=True)
43
+
44
+ uid, gid = _resolve_uid_gid()
45
+ passwd_lines = ["root:x:0:0:root:/tmp/home:/bin/sh"]
46
+ if uid != 0:
47
+ passwd_lines.append(f"user:x:{uid}:{gid}:user:/tmp/home:/bin/sh")
48
+ _write_if_changed(root / "passwd", "\n".join(passwd_lines) + "\n")
49
+
50
+ group_lines = ["root:x:0:"]
51
+ if gid != 0:
52
+ group_lines.append(f"user:x:{gid}:")
53
+ _write_if_changed(root / "group", "\n".join(group_lines) + "\n")
54
+
55
+ _write_if_changed(
56
+ root / "nsswitch.conf",
57
+ "passwd: files\ngroup: files\nhosts: files dns\n",
58
+ )
59
+ _write_if_changed(root / "hosts", "127.0.0.1 localhost\n::1 localhost\n")
60
+
61
+ resolv_path = root / "resolv.conf"
62
+ if mode == "on":
63
+ host_resolv = Path("/etc/resolv.conf")
64
+ try:
65
+ if host_resolv.exists():
66
+ _write_if_changed(resolv_path, host_resolv.read_text(encoding="utf-8"))
67
+ else:
68
+ _write_if_changed(resolv_path, "")
69
+ except OSError:
70
+ _write_if_changed(resolv_path, "")
71
+ else:
72
+ _write_if_changed(resolv_path, "")
73
+
74
+ (root / "ssl").mkdir(parents=True, exist_ok=True)
75
+ (root / "ssl" / "certs").mkdir(parents=True, exist_ok=True)
76
+ return root
@@ -0,0 +1,41 @@
1
+ """Shared cooperative-cancellation primitives.
2
+
3
+ One token discipline for every long-running execution surface (IDE bridge
4
+ jobs, Forge swarm workers): a ``threading.Event`` is the single cancel
5
+ signal, a token wraps it for duck-typed checkpoints, and cancellation
6
+ surfaces as a dedicated exception type so callers can distinguish a
7
+ cooperative stop from a failure.
8
+
9
+ The agent turn engine (``agent/turn/core.py``) duck-types tokens: it calls
10
+ ``throw_if_cancelled()`` when available, otherwise checks ``is_cancelled``.
11
+ Both behaviors are provided here.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import threading
17
+
18
+
19
+ class CooperativeCancellationError(Exception):
20
+ """Raised at a cooperative checkpoint after cancellation was requested."""
21
+
22
+ def __init__(self, reason: str = "cancelled_by_user") -> None:
23
+ super().__init__(reason)
24
+ self.reason = str(reason or "cancelled_by_user")
25
+
26
+
27
+ class EventCancellationToken:
28
+ """Duck-typed cancellation token backed by a ``threading.Event``."""
29
+
30
+ error_class: type[CooperativeCancellationError] = CooperativeCancellationError
31
+
32
+ def __init__(self, event: threading.Event) -> None:
33
+ self._event = event
34
+
35
+ @property
36
+ def is_cancelled(self) -> bool:
37
+ return self._event.is_set()
38
+
39
+ def throw_if_cancelled(self, reason: str = "cancelled_by_user") -> None:
40
+ if self.is_cancelled:
41
+ raise self.error_class(reason)