aria-code 0.167.0__py3-none-win_amd64.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 (601) hide show
  1. aria_bots/__init__.py +7 -0
  2. aria_bots/channels/README.md +97 -0
  3. aria_bots/channels/__init__.py +1 -0
  4. aria_bots/channels/approvals.py +263 -0
  5. aria_bots/channels/conversation.py +280 -0
  6. aria_bots/channels/digest.py +186 -0
  7. aria_bots/channels/registry.py +95 -0
  8. aria_bots/engine.py +144 -0
  9. aria_bots/feishu_bot.py +1385 -0
  10. aria_bots/logistics/__init__.py +1 -0
  11. aria_bots/logistics/carriers.py +253 -0
  12. aria_bots/logistics/freight.py +154 -0
  13. aria_bots/logistics/inventory.py +242 -0
  14. aria_bots/logistics/mcp_server.py +156 -0
  15. aria_bots/logistics/records.py +119 -0
  16. aria_bots/logistics/tenancy.py +152 -0
  17. aria_bots/market.py +91 -0
  18. aria_bots/relay_client.py +326 -0
  19. aria_bots/telegram_bot.py +299 -0
  20. aria_code/__init__.py +111 -0
  21. aria_code/_bin/aria-native.exe +0 -0
  22. aria_code/_bin/aria-runtime.exe +0 -0
  23. aria_code/_version.py +20 -0
  24. aria_code/account_memory.py +296 -0
  25. aria_code/account_session.py +166 -0
  26. aria_code/adk_apps/__init__.py +1 -0
  27. aria_code/adk_apps/aria_code_review/__init__.py +1 -0
  28. aria_code/adk_apps/aria_code_review/agent.py +32 -0
  29. aria_code/adk_apps/aria_research/__init__.py +1 -0
  30. aria_code/adk_apps/aria_research/agent.py +31 -0
  31. aria_code/agents/__init__.py +49 -0
  32. aria_code/agents/base.py +329 -0
  33. aria_code/agents/code_review.py +200 -0
  34. aria_code/agents/deep/__init__.py +37 -0
  35. aria_code/agents/deep/calibration_loop.py +144 -0
  36. aria_code/agents/deep/critic.py +125 -0
  37. aria_code/agents/deep/deepen.py +193 -0
  38. aria_code/agents/deep/models.py +149 -0
  39. aria_code/agents/deep/pipeline.py +164 -0
  40. aria_code/agents/deep/quant_fusion.py +192 -0
  41. aria_code/agents/deep/themes.py +95 -0
  42. aria_code/agents/deep/tiers.py +106 -0
  43. aria_code/agents/engineering/__init__.py +14 -0
  44. aria_code/agents/engineering/coder.py +222 -0
  45. aria_code/agents/engineering/tester.py +145 -0
  46. aria_code/agents/financial/__init__.py +21 -0
  47. aria_code/agents/financial/cashflow_burn.py +119 -0
  48. aria_code/agents/financial/catalyst.py +279 -0
  49. aria_code/agents/financial/corporate_finance.py +175 -0
  50. aria_code/agents/financial/debate.py +224 -0
  51. aria_code/agents/financial/earnings.py +315 -0
  52. aria_code/agents/financial/fundamental.py +178 -0
  53. aria_code/agents/financial/macro.py +111 -0
  54. aria_code/agents/financial/news.py +231 -0
  55. aria_code/agents/financial/risk.py +146 -0
  56. aria_code/agents/financial/sector.py +279 -0
  57. aria_code/agents/financial/strategist.py +121 -0
  58. aria_code/agents/financial/stripe_revenue.py +127 -0
  59. aria_code/agents/financial/synthesis.py +274 -0
  60. aria_code/agents/financial/technical.py +277 -0
  61. aria_code/agents/orchestrator.py +44 -0
  62. aria_code/agents/portfolio_agent.py +392 -0
  63. aria_code/agents/realty/__init__.py +62 -0
  64. aria_code/agents/realty/asset_diagnosis.py +152 -0
  65. aria_code/agents/realty/business_match.py +166 -0
  66. aria_code/agents/realty/cashflow_verify.py +209 -0
  67. aria_code/agents/realty/contract_rules.py +210 -0
  68. aria_code/agents/realty/energy_anomaly.py +189 -0
  69. aria_code/agents/realty/exit_settlement.py +208 -0
  70. aria_code/agents/realty/fulfillment_risk.py +206 -0
  71. aria_code/agents/realty/ops_optimize.py +160 -0
  72. aria_code/agents/realty/revenue_share.py +215 -0
  73. aria_code/agents/registry.py +168 -0
  74. aria_code/agents/signal_scheme.py +111 -0
  75. aria_code/agents/sports/__init__.py +0 -0
  76. aria_code/agents/sports/football_agent.py +197 -0
  77. aria_code/agents/supervisor.py +113 -0
  78. aria_code/agents/team.py +500 -0
  79. aria_code/agents/video/__init__.py +16 -0
  80. aria_code/agents/video/director.py +376 -0
  81. aria_code/agents/video/qa_agent.py +143 -0
  82. aria_code/agents/video/script_doctor.py +112 -0
  83. aria_code/agents/warehouse/__init__.py +25 -0
  84. aria_code/agents/warehouse/contracts.py +26 -0
  85. aria_code/agents/warehouse/dashboard.py +155 -0
  86. aria_code/agents/warehouse/fulfillment_leadtime.py +95 -0
  87. aria_code/agents/warehouse/inbound_exceptions.py +25 -0
  88. aria_code/agents/warehouse/inventory_health.py +24 -0
  89. aria_code/agents/warehouse/local_run.py +71 -0
  90. aria_code/agents/warehouse/local_snapshot.py +132 -0
  91. aria_code/agents/warehouse/logistics_cost.py +84 -0
  92. aria_code/agents/warehouse/logistics_sync.py +21 -0
  93. aria_code/agents/warehouse/navigation.schema.json +110 -0
  94. aria_code/agents/warehouse/task_agent.py +172 -0
  95. aria_code/agents/warehouse/workflow.py +31 -0
  96. aria_code/aliyun_data_client.py +4 -0
  97. aria_code/apps/README.md +12 -0
  98. aria_code/apps/__init__.py +2 -0
  99. aria_code/apps/channels/README.md +9 -0
  100. aria_code/apps/channels/__init__.py +17 -0
  101. aria_code/apps/channels/approvals.py +11 -0
  102. aria_code/apps/channels/conversation.py +11 -0
  103. aria_code/apps/channels/digest.py +11 -0
  104. aria_code/apps/channels/intake.py +92 -0
  105. aria_code/apps/channels/registry.py +11 -0
  106. aria_code/apps/channels/tradingview.py +94 -0
  107. aria_code/apps/cli/README.md +13 -0
  108. aria_code/apps/cli/__init__.py +2 -0
  109. aria_code/apps/cli/_optional.py +120 -0
  110. aria_code/apps/cli/app_server.py +656 -0
  111. aria_code/apps/cli/auto_update.py +125 -0
  112. aria_code/apps/cli/bootstrap.py +206 -0
  113. aria_code/apps/cli/broker_render.py +206 -0
  114. aria_code/apps/cli/chat_turn.py +1327 -0
  115. aria_code/apps/cli/cloud_models.py +231 -0
  116. aria_code/apps/cli/codegen_paths.py +29 -0
  117. aria_code/apps/cli/commands/__init__.py +34 -0
  118. aria_code/apps/cli/commands/_business_render.py +78 -0
  119. aria_code/apps/cli/commands/_ui.py +71 -0
  120. aria_code/apps/cli/commands/analysis_cmds.py +378 -0
  121. aria_code/apps/cli/commands/ashare_prediction_cmds.py +327 -0
  122. aria_code/apps/cli/commands/auth_cmds.py +215 -0
  123. aria_code/apps/cli/commands/backtest_cmds.py +2518 -0
  124. aria_code/apps/cli/commands/broker_cmds.py +1236 -0
  125. aria_code/apps/cli/commands/business_workflow_cmds.py +379 -0
  126. aria_code/apps/cli/commands/canvas_cmds.py +77 -0
  127. aria_code/apps/cli/commands/catalog.py +189 -0
  128. aria_code/apps/cli/commands/core_cmds.py +2748 -0
  129. aria_code/apps/cli/commands/data_cmds.py +521 -0
  130. aria_code/apps/cli/commands/diagnostic_cmds.py +285 -0
  131. aria_code/apps/cli/commands/diagnostic_ops_cmds.py +813 -0
  132. aria_code/apps/cli/commands/engineering_cmds.py +399 -0
  133. aria_code/apps/cli/commands/file_cmds.py +312 -0
  134. aria_code/apps/cli/commands/finance_service_cmds.py +60 -0
  135. aria_code/apps/cli/commands/fx_commodity_cmds.py +123 -0
  136. aria_code/apps/cli/commands/market.py +544 -0
  137. aria_code/apps/cli/commands/market_cmds.py +1419 -0
  138. aria_code/apps/cli/commands/market_context.py +432 -0
  139. aria_code/apps/cli/commands/market_render.py +7 -0
  140. aria_code/apps/cli/commands/model_cmds.py +2374 -0
  141. aria_code/apps/cli/commands/ops_cmds.py +721 -0
  142. aria_code/apps/cli/commands/orchestrator_cmds.py +181 -0
  143. aria_code/apps/cli/commands/pdf_export_cmds.py +125 -0
  144. aria_code/apps/cli/commands/portfolio_cmds.py +1316 -0
  145. aria_code/apps/cli/commands/report.py +476 -0
  146. aria_code/apps/cli/commands/scaffold_templates.py +617 -0
  147. aria_code/apps/cli/commands/session_cmds.py +236 -0
  148. aria_code/apps/cli/commands/session_ux_cmds.py +359 -0
  149. aria_code/apps/cli/commands/team.py +723 -0
  150. aria_code/apps/cli/commands/team_render.py +8 -0
  151. aria_code/apps/cli/commands/ui_cmds.py +401 -0
  152. aria_code/apps/cli/commands/warehouse_cmds.py +297 -0
  153. aria_code/apps/cli/commands/workflow_cmds.py +629 -0
  154. aria_code/apps/cli/commands/workspace_cmds.py +1543 -0
  155. aria_code/apps/cli/config_paths.py +73 -0
  156. aria_code/apps/cli/config_store.py +158 -0
  157. aria_code/apps/cli/constants.py +88 -0
  158. aria_code/apps/cli/context.py +34 -0
  159. aria_code/apps/cli/conversation_policy.py +53 -0
  160. aria_code/apps/cli/deterministic.py +200 -0
  161. aria_code/apps/cli/direct.py +48 -0
  162. aria_code/apps/cli/earnings_view.py +172 -0
  163. aria_code/apps/cli/endpoint_migration.py +64 -0
  164. aria_code/apps/cli/exec_events.py +255 -0
  165. aria_code/apps/cli/file_references.py +155 -0
  166. aria_code/apps/cli/finance_service_catalog.py +111 -0
  167. aria_code/apps/cli/football_reports.py +231 -0
  168. aria_code/apps/cli/github_app_auth.py +135 -0
  169. aria_code/apps/cli/google_login.py +239 -0
  170. aria_code/apps/cli/handlers/__init__.py +11 -0
  171. aria_code/apps/cli/handlers/broker_handlers.py +122 -0
  172. aria_code/apps/cli/handlers/chart_handlers.py +1314 -0
  173. aria_code/apps/cli/handlers/market_handlers.py +2570 -0
  174. aria_code/apps/cli/handlers/realty_handlers.py +114 -0
  175. aria_code/apps/cli/handlers/sports_handlers.py +22 -0
  176. aria_code/apps/cli/handlers/strategy_advice.py +82 -0
  177. aria_code/apps/cli/headless.py +352 -0
  178. aria_code/apps/cli/helpers.py +326 -0
  179. aria_code/apps/cli/hooks.py +180 -0
  180. aria_code/apps/cli/i18n.py +310 -0
  181. aria_code/apps/cli/intent.py +136 -0
  182. aria_code/apps/cli/intent_router.py +305 -0
  183. aria_code/apps/cli/intent_shadow.py +72 -0
  184. aria_code/apps/cli/intent_signals.py +347 -0
  185. aria_code/apps/cli/lifecycle_hooks.py +48 -0
  186. aria_code/apps/cli/local_tool_schemas.py +550 -0
  187. aria_code/apps/cli/main.py +39 -0
  188. aria_code/apps/cli/market_hours.py +100 -0
  189. aria_code/apps/cli/market_metadata.py +135 -0
  190. aria_code/apps/cli/market_universe.py +487 -0
  191. aria_code/apps/cli/message_processing.py +310 -0
  192. aria_code/apps/cli/mixin_binding.py +32 -0
  193. aria_code/apps/cli/model_catalog.py +652 -0
  194. aria_code/apps/cli/model_probe.py +169 -0
  195. aria_code/apps/cli/model_retirement.py +65 -0
  196. aria_code/apps/cli/native_branding.py +12 -0
  197. aria_code/apps/cli/native_bridge.py +118 -0
  198. aria_code/apps/cli/native_entry.py +58 -0
  199. aria_code/apps/cli/pdf_report.py +977 -0
  200. aria_code/apps/cli/plan_mode.py +139 -0
  201. aria_code/apps/cli/plotly_html.py +15 -0
  202. aria_code/apps/cli/prediction_feedback.py +202 -0
  203. aria_code/apps/cli/preflight.py +502 -0
  204. aria_code/apps/cli/project_aria.py +60 -0
  205. aria_code/apps/cli/prompt_assembly.py +78 -0
  206. aria_code/apps/cli/prompts/__init__.py +0 -0
  207. aria_code/apps/cli/prompts/coding.py +71 -0
  208. aria_code/apps/cli/prompts/select.py +193 -0
  209. aria_code/apps/cli/prompts/system_prompts.py +562 -0
  210. aria_code/apps/cli/prompts/ui.py +436 -0
  211. aria_code/apps/cli/provider_endpoints.py +237 -0
  212. aria_code/apps/cli/provider_keys.py +73 -0
  213. aria_code/apps/cli/providers/__init__.py +1 -0
  214. aria_code/apps/cli/providers/base.py +775 -0
  215. aria_code/apps/cli/providers/chat_routing.py +139 -0
  216. aria_code/apps/cli/providers/collaboration.py +98 -0
  217. aria_code/apps/cli/providers/llm/__init__.py +1 -0
  218. aria_code/apps/cli/providers/llm/offline_responses.py +68 -0
  219. aria_code/apps/cli/providers/llm/ollama_stream.py +1574 -0
  220. aria_code/apps/cli/providers/llm/response_cache.py +60 -0
  221. aria_code/apps/cli/providers/llm/sse_stream.py +298 -0
  222. aria_code/apps/cli/providers/runtime_bridge.py +586 -0
  223. aria_code/apps/cli/providers/vertexai_stream.py +592 -0
  224. aria_code/apps/cli/release_download.py +159 -0
  225. aria_code/apps/cli/review_runner.py +111 -0
  226. aria_code/apps/cli/runtime_consumer.py +769 -0
  227. aria_code/apps/cli/sandbox.py +32 -0
  228. aria_code/apps/cli/session_export.py +120 -0
  229. aria_code/apps/cli/session_jsonl.py +208 -0
  230. aria_code/apps/cli/session_refresh.py +123 -0
  231. aria_code/apps/cli/session_store.py +125 -0
  232. aria_code/apps/cli/skills_catalog.py +273 -0
  233. aria_code/apps/cli/task_isolation.py +211 -0
  234. aria_code/apps/cli/terminal_chrome.py +412 -0
  235. aria_code/apps/cli/todo_tracker.py +190 -0
  236. aria_code/apps/cli/tool_executor.py +932 -0
  237. aria_code/apps/cli/tool_registry.py +34 -0
  238. aria_code/apps/cli/tool_scope.py +113 -0
  239. aria_code/apps/cli/tools/__init__.py +40 -0
  240. aria_code/apps/cli/tools/context.py +46 -0
  241. aria_code/apps/cli/tools/file_tools.py +181 -0
  242. aria_code/apps/cli/tools/market_tools.py +626 -0
  243. aria_code/apps/cli/tools/notebook_tools.py +111 -0
  244. aria_code/apps/cli/tools/system_tools.py +656 -0
  245. aria_code/apps/cli/tools/write_tools.py +961 -0
  246. aria_code/apps/cli/tradingview_bridge.py +529 -0
  247. aria_code/apps/cli/transactions.py +269 -0
  248. aria_code/apps/cli/turn_planning.py +137 -0
  249. aria_code/apps/cli/update_check.py +257 -0
  250. aria_code/apps/cli/updater.py +373 -0
  251. aria_code/apps/cli/utils/__init__.py +0 -0
  252. aria_code/apps/cli/utils/market_detect.py +1663 -0
  253. aria_code/apps/cli/web_login.py +136 -0
  254. aria_code/apps/cli/workflow_runner.py +127 -0
  255. aria_code/apps/cli/workspace_route.py +121 -0
  256. aria_code/apps/daemon/README.md +14 -0
  257. aria_code/apps/vscode/README.md +115 -0
  258. aria_code/apps/vscode/package.json +70 -0
  259. aria_code/aria_cli.py +5244 -0
  260. aria_code/aria_daemon.py +1372 -0
  261. aria_code/aria_feishu_bot.py +16 -0
  262. aria_code/aria_mcp_server.py +61 -0
  263. aria_code/aria_relay_client.py +4 -0
  264. aria_code/aria_telegram_bot.py +11 -0
  265. aria_code/ariarc.py +385 -0
  266. aria_code/artifacts.py +581 -0
  267. aria_code/backtest_engine.py +4 -0
  268. aria_code/backtest_report.py +4 -0
  269. aria_code/brokers/__init__.py +78 -0
  270. aria_code/brokers/automation.py +193 -0
  271. aria_code/brokers/base.py +231 -0
  272. aria_code/brokers/capabilities.py +264 -0
  273. aria_code/brokers/cn/__init__.py +10 -0
  274. aria_code/brokers/cn/easytrader_broker.py +193 -0
  275. aria_code/brokers/cn/futu_broker.py +194 -0
  276. aria_code/brokers/cn/longbridge_broker.py +190 -0
  277. aria_code/brokers/cn/tiger_broker.py +196 -0
  278. aria_code/brokers/cn/xtquant_broker.py +225 -0
  279. aria_code/brokers/config.py +414 -0
  280. aria_code/brokers/intl/__init__.py +5 -0
  281. aria_code/brokers/intl/alpaca_broker.py +183 -0
  282. aria_code/brokers/intl/ibkr_broker.py +215 -0
  283. aria_code/brokers/intl/trading212_broker.py +183 -0
  284. aria_code/brokers/intl/webull_broker.py +156 -0
  285. aria_code/brokers/paper_broker.py +266 -0
  286. aria_code/brokers/planning.py +296 -0
  287. aria_code/brokers/registry.py +213 -0
  288. aria_code/brokers/trading.py +267 -0
  289. aria_code/canva_client.py +4 -0
  290. aria_code/change_store.py +127 -0
  291. aria_code/clients/__init__.py +0 -0
  292. aria_code/clients/aliyun_data_client.py +660 -0
  293. aria_code/clients/aria_relay_client.py +16 -0
  294. aria_code/clients/canva_client.py +284 -0
  295. aria_code/clients/figma_client.py +100 -0
  296. aria_code/clients/football_data_client.py +1671 -0
  297. aria_code/clients/kling_video_client.py +182 -0
  298. aria_code/clients/market_data_client.py +1985 -0
  299. aria_code/clients/openai_image_client.py +242 -0
  300. aria_code/clients/runway_video_client.py +157 -0
  301. aria_code/clients/warehouse_erp_client.py +105 -0
  302. aria_code/cloud_memory.py +190 -0
  303. aria_code/command_safety.py +19 -0
  304. aria_code/computer_use_server.py +276 -0
  305. aria_code/computer_use_tools.py +4 -0
  306. aria_code/dashboard_generator.py +579 -0
  307. aria_code/data_analysis_tools.py +4 -0
  308. aria_code/data_cleaner.py +483 -0
  309. aria_code/data_service.py +513 -0
  310. aria_code/datasources/__init__.py +23 -0
  311. aria_code/datasources/base.py +166 -0
  312. aria_code/datasources/router.py +253 -0
  313. aria_code/datasources/sec_statements.py +217 -0
  314. aria_code/datasources/sources/__init__.py +15 -0
  315. aria_code/datasources/sources/akshare_source.py +269 -0
  316. aria_code/datasources/sources/alpha_vantage_source.py +203 -0
  317. aria_code/datasources/sources/edgar_source.py +219 -0
  318. aria_code/datasources/sources/finnhub_source.py +198 -0
  319. aria_code/datasources/sources/fred_source.py +223 -0
  320. aria_code/datasources/sources/tushare_source.py +143 -0
  321. aria_code/datasources/sources/web_scraper_source.py +278 -0
  322. aria_code/datasources/sources/world_bank_source.py +206 -0
  323. aria_code/datasources/sources/yfinance_source.py +152 -0
  324. aria_code/doctor.py +860 -0
  325. aria_code/domain/__init__.py +0 -0
  326. aria_code/domain/_finance_core/__init__.py +5 -0
  327. aria_code/domain/_finance_core/backtest_engine.py +325 -0
  328. aria_code/domain/_finance_core/performance_stats.py +101 -0
  329. aria_code/domain/backtest_engine.py +75 -0
  330. aria_code/domain/backtest_report.py +488 -0
  331. aria_code/domain/finance_formulas.py +389 -0
  332. aria_code/domain/performance_stats.py +6 -0
  333. aria_code/domain/portfolio_ledger.py +307 -0
  334. aria_code/domain/strategy_vault.py +421 -0
  335. aria_code/evals/__init__.py +37 -0
  336. aria_code/evals/behavior.py +113 -0
  337. aria_code/evals/gap.py +93 -0
  338. aria_code/evals/harness.py +949 -0
  339. aria_code/evals/review_probe.py +132 -0
  340. aria_code/evals/runner.py +425 -0
  341. aria_code/evals/scorecard.py +322 -0
  342. aria_code/evals/summary.py +125 -0
  343. aria_code/external_agent_runner.py +115 -0
  344. aria_code/figma_client.py +4 -0
  345. aria_code/file_analysis_tools.py +4 -0
  346. aria_code/finance_formulas.py +4 -0
  347. aria_code/football_data_client.py +4 -0
  348. aria_code/image_gen_tools.py +4 -0
  349. aria_code/intent_classifier.py +459 -0
  350. aria_code/kling_video_client.py +4 -0
  351. aria_code/licensing.py +151 -0
  352. aria_code/local_finance_tools.py +4 -0
  353. aria_code/local_image_provider.py +4 -0
  354. aria_code/local_llm_provider.py +4 -0
  355. aria_code/macro_tools.py +4 -0
  356. aria_code/markdown_pdf.py +289 -0
  357. aria_code/market_data_client.py +4 -0
  358. aria_code/mcp_client.py +744 -0
  359. aria_code/memory_manager.py +273 -0
  360. aria_code/model_capability.py +512 -0
  361. aria_code/notification_tools.py +4 -0
  362. aria_code/openai_image_client.py +4 -0
  363. aria_code/packages/__init__.py +26 -0
  364. aria_code/packages/adk_bridge/__init__.py +11 -0
  365. aria_code/packages/adk_bridge/code_review_tools.py +49 -0
  366. aria_code/packages/adk_bridge/market_tools.py +125 -0
  367. aria_code/packages/aria_agents/__init__.py +5 -0
  368. aria_code/packages/aria_agents/manifest.py +78 -0
  369. aria_code/packages/aria_core/__init__.py +34 -0
  370. aria_code/packages/aria_core/architecture.py +269 -0
  371. aria_code/packages/aria_core/export.py +147 -0
  372. aria_code/packages/aria_core/manifest.py +65 -0
  373. aria_code/packages/aria_core/paths.py +57 -0
  374. aria_code/packages/aria_core/secure_file.py +68 -0
  375. aria_code/packages/aria_infra/__init__.py +15 -0
  376. aria_code/packages/aria_infra/arthera.py +58 -0
  377. aria_code/packages/aria_infra/doctor.py +246 -0
  378. aria_code/packages/aria_infra/product.py +40 -0
  379. aria_code/packages/aria_mcp/__init__.py +25 -0
  380. aria_code/packages/aria_mcp/bridge.py +232 -0
  381. aria_code/packages/aria_mcp/circuit.py +82 -0
  382. aria_code/packages/aria_mcp/config.py +162 -0
  383. aria_code/packages/aria_mcp/installer.py +95 -0
  384. aria_code/packages/aria_mcp/server.py +1705 -0
  385. aria_code/packages/aria_mcp/tools.py +81 -0
  386. aria_code/packages/aria_sdk/__init__.py +19 -0
  387. aria_code/packages/aria_sdk/client.py +393 -0
  388. aria_code/packages/aria_sdk/providers.py +70 -0
  389. aria_code/packages/aria_sdk/streaming.py +75 -0
  390. aria_code/packages/aria_sdk/types.py +86 -0
  391. aria_code/packages/aria_services/__init__.py +92 -0
  392. aria_code/packages/aria_services/cache.py +60 -0
  393. aria_code/packages/aria_services/context.py +356 -0
  394. aria_code/packages/aria_services/context_checkpoints.py +157 -0
  395. aria_code/packages/aria_services/data.py +11 -0
  396. aria_code/packages/aria_services/provider_health.py +304 -0
  397. aria_code/packages/aria_services/references.py +247 -0
  398. aria_code/packages/aria_services/registry.py +249 -0
  399. aria_code/packages/aria_services/research_protocol.py +137 -0
  400. aria_code/packages/aria_services/research_quality.py +186 -0
  401. aria_code/packages/aria_services/settings.py +160 -0
  402. aria_code/packages/aria_services/usage.py +151 -0
  403. aria_code/packages/aria_skills/__init__.py +46 -0
  404. aria_code/packages/aria_skills/catalog.py +149 -0
  405. aria_code/packages/aria_skills/loader.py +455 -0
  406. aria_code/packages/aria_skills/registry.py +84 -0
  407. aria_code/packages/aria_tools/__init__.py +5 -0
  408. aria_code/packages/aria_tools/financial/__init__.py +5 -0
  409. aria_code/packages/aria_tools/financial/_paths.py +92 -0
  410. aria_code/packages/aria_tools/registry.py +129 -0
  411. aria_code/packages/quant_engine/__init__.py +33 -0
  412. aria_code/packages/quant_engine/_namespace.py +24 -0
  413. aria_code/packages/quant_engine/backtest/__init__.py +10 -0
  414. aria_code/packages/quant_engine/backtest/core.py +53 -0
  415. aria_code/packages/quant_engine/backtest/engine.py +102 -0
  416. aria_code/packages/quant_engine/portfolio/__init__.py +9 -0
  417. aria_code/packages/quant_engine/portfolio/optimizer.py +85 -0
  418. aria_code/packages/quant_engine/sports/__init__.py +76 -0
  419. aria_code/packages/quant_engine/sports/calibrator.py +354 -0
  420. aria_code/packages/quant_engine/sports/dixon_coles.py +234 -0
  421. aria_code/packages/quant_engine/sports/elo.py +300 -0
  422. aria_code/packages/quant_engine/sports/form.py +188 -0
  423. aria_code/packages/quant_engine/sports/h2h.py +195 -0
  424. aria_code/packages/quant_engine/sports/predictor.py +312 -0
  425. aria_code/packages/quant_engine/sports/tracker.py +665 -0
  426. aria_code/packages/quant_engine/stochastic/__init__.py +31 -0
  427. aria_code/packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
  428. aria_code/packages/quant_engine/stochastic/ito_calculus.py +477 -0
  429. aria_code/packages/quant_engine/stochastic/kelly_criterion.py +181 -0
  430. aria_code/packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
  431. aria_code/packages/quant_engine/stochastic/options_pricing.py +576 -0
  432. aria_code/packages/quant_engine/stochastic/stochastic_processes.py +90 -0
  433. aria_code/packs/__init__.py +85 -0
  434. aria_code/packs/base.py +168 -0
  435. aria_code/packs/finance/__init__.py +250 -0
  436. aria_code/packs/healthcare/__init__.py +40 -0
  437. aria_code/packs/logistics/__init__.py +171 -0
  438. aria_code/packs/payments/__init__.py +139 -0
  439. aria_code/packs/realty/__init__.py +190 -0
  440. aria_code/packs/registry.py +202 -0
  441. aria_code/packs/rules.py +90 -0
  442. aria_code/plan_utils.py +194 -0
  443. aria_code/plugin_loader.py +328 -0
  444. aria_code/portfolio_ledger.py +4 -0
  445. aria_code/preview_server.py +449 -0
  446. aria_code/privacy/__init__.py +5 -0
  447. aria_code/privacy/feedback.py +123 -0
  448. aria_code/project_review.py +278 -0
  449. aria_code/project_review_api.py +235 -0
  450. aria_code/project_review_server.py +18 -0
  451. aria_code/project_tools.py +4 -0
  452. aria_code/providers/__init__.py +30 -0
  453. aria_code/providers/google_video_provider.py +256 -0
  454. aria_code/providers/llm/__init__.py +19 -0
  455. aria_code/providers/llm/anthropic.py +184 -0
  456. aria_code/providers/llm/arthera_gateway.py +90 -0
  457. aria_code/providers/llm/autoconfig.py +213 -0
  458. aria_code/providers/llm/base.py +139 -0
  459. aria_code/providers/llm/custom.py +158 -0
  460. aria_code/providers/llm/ollama.py +137 -0
  461. aria_code/providers/llm/openai_compat.py +350 -0
  462. aria_code/providers/llm/registry.py +457 -0
  463. aria_code/providers/local_image_provider.py +202 -0
  464. aria_code/providers/local_llm_provider.py +549 -0
  465. aria_code/providers/ollama_models.py +79 -0
  466. aria_code/providers/openai_params.py +32 -0
  467. aria_code/providers/tool_messages.py +100 -0
  468. aria_code/realty_data_tools.py +4 -0
  469. aria_code/report_exporters.py +150 -0
  470. aria_code/report_generator.py +1764 -0
  471. aria_code/review_service.py +497 -0
  472. aria_code/runtime/__init__.py +176 -0
  473. aria_code/runtime/acceptance.py +632 -0
  474. aria_code/runtime/agent_loop.py +1851 -0
  475. aria_code/runtime/agent_profiles.py +75 -0
  476. aria_code/runtime/approval.py +71 -0
  477. aria_code/runtime/baseline.py +219 -0
  478. aria_code/runtime/budget.py +232 -0
  479. aria_code/runtime/checkpoints.py +481 -0
  480. aria_code/runtime/consult.py +65 -0
  481. aria_code/runtime/contract.py +282 -0
  482. aria_code/runtime/delivery.py +432 -0
  483. aria_code/runtime/events.py +113 -0
  484. aria_code/runtime/gateway.py +167 -0
  485. aria_code/runtime/lsp.py +484 -0
  486. aria_code/runtime/native_index.py +153 -0
  487. aria_code/runtime/orchestrator.py +55 -0
  488. aria_code/runtime/processes.py +343 -0
  489. aria_code/runtime/project_graph.py +614 -0
  490. aria_code/runtime/repo_map.py +712 -0
  491. aria_code/runtime/research_bundle.py +105 -0
  492. aria_code/runtime/review.py +269 -0
  493. aria_code/runtime/run_state.py +127 -0
  494. aria_code/runtime/run_store.py +522 -0
  495. aria_code/runtime/scoped_runs.py +113 -0
  496. aria_code/runtime/self_healing.py +422 -0
  497. aria_code/runtime/semantic_diff.py +171 -0
  498. aria_code/runtime/subagent.py +721 -0
  499. aria_code/runtime/symbol_edit.py +117 -0
  500. aria_code/runtime/task_brief.py +46 -0
  501. aria_code/runtime/task_context.py +145 -0
  502. aria_code/runtime/task_ledger.py +108 -0
  503. aria_code/runtime/task_orchestrator.py +90 -0
  504. aria_code/runtime/task_outcome.py +33 -0
  505. aria_code/runtime/task_verification.py +266 -0
  506. aria_code/runtime/task_worktree.py +448 -0
  507. aria_code/runtime/tool_executor.py +329 -0
  508. aria_code/runtime/tool_policy.py +107 -0
  509. aria_code/runtime/transactions.py +216 -0
  510. aria_code/runtime/windows_job.py +115 -0
  511. aria_code/runtime/workflows.py +222 -0
  512. aria_code/runtime/worktrees.py +215 -0
  513. aria_code/runway_video_client.py +4 -0
  514. aria_code/safety/__init__.py +27 -0
  515. aria_code/safety/client_isolation.py +178 -0
  516. aria_code/safety/permissions.py +339 -0
  517. aria_code/safety/risk.py +699 -0
  518. aria_code/safety/sandbox.py +148 -0
  519. aria_code/safety/service.py +102 -0
  520. aria_code/service_endpoints.py +14 -0
  521. aria_code/setup_wizard.py +684 -0
  522. aria_code/spreadsheet_tools.py +4 -0
  523. aria_code/strategy_vault.py +4 -0
  524. aria_code/tools/__init__.py +0 -0
  525. aria_code/tools/artifact_tools.py +252 -0
  526. aria_code/tools/broker_tools.py +92 -0
  527. aria_code/tools/build_quant_engine.py +97 -0
  528. aria_code/tools/code_audit_tools.py +192 -0
  529. aria_code/tools/computer_use_tools.py +509 -0
  530. aria_code/tools/data_analysis_tools.py +831 -0
  531. aria_code/tools/enterprise_finance_tools.py +202 -0
  532. aria_code/tools/extended_tools.py +240 -0
  533. aria_code/tools/file_analysis_tools.py +880 -0
  534. aria_code/tools/image_gen_tools.py +114 -0
  535. aria_code/tools/local_finance_tools.py +3488 -0
  536. aria_code/tools/logistics_carriers.py +11 -0
  537. aria_code/tools/logistics_inventory.py +11 -0
  538. aria_code/tools/logistics_io.py +11 -0
  539. aria_code/tools/logistics_tenancy.py +11 -0
  540. aria_code/tools/logistics_tools.py +11 -0
  541. aria_code/tools/macro_tools.py +412 -0
  542. aria_code/tools/notification_tools.py +249 -0
  543. aria_code/tools/project_tools.py +525 -0
  544. aria_code/tools/realty_data_tools.py +662 -0
  545. aria_code/tools/spreadsheet_tools.py +399 -0
  546. aria_code/tools/stripe_tools.py +115 -0
  547. aria_code/tools/video_tools.py +500 -0
  548. aria_code/training/__init__.py +1 -0
  549. aria_code/training/compare.py +117 -0
  550. aria_code/training/prelude_commands.py +111 -0
  551. aria_code/training/prelude_eval.py +390 -0
  552. aria_code/training/prelude_real.py +183 -0
  553. aria_code/training/prelude_train.py +248 -0
  554. aria_code/training/sft_dataset.py +230 -0
  555. aria_code/training/trajectory_dataset.py +263 -0
  556. aria_code/ui/__init__.py +105 -0
  557. aria_code/ui/banner.py +404 -0
  558. aria_code/ui/completer.py +571 -0
  559. aria_code/ui/console.py +285 -0
  560. aria_code/ui/input_box.py +505 -0
  561. aria_code/ui/mentions.py +206 -0
  562. aria_code/ui/picker.py +248 -0
  563. aria_code/ui/render/__init__.py +11 -0
  564. aria_code/ui/render/actions.py +297 -0
  565. aria_code/ui/render/finance.py +1732 -0
  566. aria_code/ui/render/market.py +253 -0
  567. aria_code/ui/render/output.py +954 -0
  568. aria_code/ui/render/responsive.py +75 -0
  569. aria_code/ui/render/team.py +367 -0
  570. aria_code/ui/robot.py +239 -0
  571. aria_code/ui/startup_dashboard.py +110 -0
  572. aria_code/video_analysis.py +147 -0
  573. aria_code/video_editor.py +518 -0
  574. aria_code/video_workflow/__init__.py +8 -0
  575. aria_code/video_workflow/__main__.py +61 -0
  576. aria_code/video_workflow/engines/__init__.py +18 -0
  577. aria_code/video_workflow/engines/base.py +69 -0
  578. aria_code/video_workflow/engines/custom_model.py +204 -0
  579. aria_code/video_workflow/engines/google_veo.py +94 -0
  580. aria_code/video_workflow/engines/local_engine.py +71 -0
  581. aria_code/video_workflow/engines/registry.py +98 -0
  582. aria_code/video_workflow/google_models.py +278 -0
  583. aria_code/video_workflow/music/__init__.py +5 -0
  584. aria_code/video_workflow/music/foley_service.py +132 -0
  585. aria_code/video_workflow/music/service.py +212 -0
  586. aria_code/video_workflow/pipeline.py +620 -0
  587. aria_code/video_workflow/plan.py +202 -0
  588. aria_code/video_workflow/session.py +297 -0
  589. aria_code/workspace/__init__.py +6 -0
  590. aria_code/workspace/files.py +219 -0
  591. aria_code/workspace/verify.py +167 -0
  592. aria_code/workspace_changes.py +199 -0
  593. aria_code/workspace_service.py +196 -0
  594. aria_code/workspace_tools.py +150 -0
  595. aria_code-0.167.0.dist-info/METADATA +330 -0
  596. aria_code-0.167.0.dist-info/RECORD +601 -0
  597. aria_code-0.167.0.dist-info/WHEEL +5 -0
  598. aria_code-0.167.0.dist-info/entry_points.txt +5 -0
  599. aria_code-0.167.0.dist-info/licenses/LICENSE +201 -0
  600. aria_code-0.167.0.dist-info/licenses/NOTICE +49 -0
  601. aria_code-0.167.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,949 @@
1
+ """Run verifiable tasks and score them by exit code.
2
+
3
+ The shape of one task
4
+ ---------------------
5
+ A fixture directory, a prompt, and a command::
6
+
7
+ - id: fix-failing-test
8
+ prompt: "tests/test_math.py 有一个测试失败了,修好它"
9
+ fixture: fix_failing_test
10
+ verify: "{python} -m pytest -q"
11
+ requires: [pytest]
12
+
13
+ The fixture is copied to a scratch directory, the agent is turned loose in the
14
+ copy, and ``verify`` decides the outcome. Exit 0 is a pass. Nothing else
15
+ counts — not a confident summary, not a diff that looks right.
16
+
17
+ The property that makes a suite trustworthy
18
+ -------------------------------------------
19
+ **Every task must start red.** Before the agent runs, the harness runs
20
+ ``verify`` on the untouched fixture. If it already passes, the task is
21
+ reported ``INVALID`` and scored as neither pass nor fail.
22
+
23
+ This check is worth more than any individual task. A suite that silently
24
+ accumulates already-green tasks reports a rising pass rate while measuring
25
+ less and less, and the failure is invisible precisely because the number looks
26
+ good — you cannot tell a task the agent solved from a task that was never
27
+ broken. Running the check every time costs one command per task and makes the
28
+ number mean something.
29
+
30
+ "Red" has to mean the check ran
31
+ ------------------------------
32
+ The pre-flight alone is not enough, and the first run of this harness proved
33
+ it: every task reported red, and every task was red because the ``python3`` on
34
+ PATH had no pytest. The suite looked healthy while measuring nothing at all —
35
+ the same silent-inflation failure the pre-flight exists to prevent, arriving
36
+ through the back door.
37
+
38
+ Two things close it. ``{python}`` in a command resolves to the interpreter
39
+ running the harness, so a suite verifies against the environment it was
40
+ launched with rather than whatever ``python3`` happens to mean on this
41
+ machine. And a task may declare ``requires: [pytest]``; a missing import is
42
+ reported as ``ERROR`` — excluded from the score — instead of being counted as
43
+ a red test the agent is expected to fix.
44
+
45
+ Isolation
46
+ ---------
47
+ Tasks run in a copy under a scratch root, never in the fixture and never in
48
+ the repository. An agent that deletes the workspace, writes outside it, or
49
+ leaves it in a broken state affects nothing but its own run's directory, which
50
+ is what makes it safe to keep destructive tasks in the suite — those are the
51
+ ones worth having. The agent also gets its own HOME / ARIA_HOME / XDG tree
52
+ per attempt (runner.trial_home_env), so nothing one attempt remembers reaches
53
+ the next.
54
+
55
+ A pass has to be the work
56
+ -------------------------
57
+ Editing a protected test is a FAIL, and so is *adding* a file that changes
58
+ how the check runs (conftest.py, pytest.ini, a .pth, a module named like
59
+ pytest or the stdlib) unless the task lists it in ``allow_new``. An exit code
60
+ of 125/126/127 is only "the check could not run" when the harness failed to
61
+ start it or a program on the command line really is missing; the agent's code
62
+ exiting 127 from inside the check is a FAIL.
63
+ """
64
+
65
+ from __future__ import annotations
66
+
67
+ import importlib.util
68
+ import json
69
+ import re
70
+ import os
71
+ import secrets
72
+ import shlex
73
+ import shutil
74
+ import subprocess
75
+ import sys
76
+ import tempfile
77
+ import time
78
+ from dataclasses import dataclass, field
79
+ from pathlib import Path
80
+ from typing import Any, Callable, Iterable, List, Optional, Sequence
81
+
82
+ __all__ = [
83
+ "SuiteResult",
84
+ "TaskResult",
85
+ "TaskSpec",
86
+ "Solver",
87
+ "load_suite",
88
+ "run_suite",
89
+ "run_task",
90
+ ]
91
+
92
+ # Outcomes. Kept as four values rather than a boolean because "the agent
93
+ # failed" and "the task was broken" call for opposite responses, and collapsing
94
+ # them is how a rotten suite goes unnoticed.
95
+ PASS = "pass"
96
+ FAIL = "fail"
97
+ INVALID = "invalid" # the fixture was already green: the task measures nothing
98
+ ERROR = "error" # the harness or the solver blew up; not the agent's score
99
+
100
+ _MAX_LOG_CHARS = 4000
101
+
102
+ # Residue that must not travel from a fixture into a task workspace.
103
+ # Directory names that hold build/run residue rather than source.
104
+ _RUN_RESIDUE = frozenset({
105
+ ".git", "__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache",
106
+ ".tox", "node_modules", ".DS_Store",
107
+ })
108
+
109
+ _IGNORED_FIXTURE_ENTRIES = shutil.ignore_patterns(
110
+ "__pycache__", "*.pyc", "*.pyo", ".pytest_cache", ".mypy_cache",
111
+ ".ruff_cache", ".git", "node_modules", ".DS_Store",
112
+ )
113
+
114
+
115
+ def _trim(text: str, limit: int = _MAX_LOG_CHARS) -> str:
116
+ body = (text or "").strip()
117
+ if len(body) <= limit:
118
+ return body
119
+ half = limit // 2
120
+ return f"{body[:half]}\n… [truncated] …\n{body[-half:]}"
121
+
122
+
123
+ @dataclass(frozen=True)
124
+ class TaskSpec:
125
+ """One verifiable task."""
126
+
127
+ id: str
128
+ prompt: str
129
+ verify: str
130
+ fixture: str = ""
131
+ timeout: int = 300
132
+ solve_timeout: int = 900
133
+ tags: tuple[str, ...] = ()
134
+ setup: tuple[str, ...] = ()
135
+ # Importable modules the check needs. Missing ones make the task ERROR
136
+ # rather than FAIL: an absent pytest is not a bug for the agent to fix.
137
+ requires: tuple[str, ...] = ()
138
+ # Files the agent must not modify. Defaults to the tests, because the
139
+ # check IS the tests: an agent that edits them can turn any task green
140
+ # without doing the work, and the score would be indistinguishable from a
141
+ # real solve. Editing one is scored FAIL no matter what the check says.
142
+ protect: tuple[str, ...] = ("test_*.py", "*_test.py", "tests/**")
143
+ # A task may legitimately start green when it is a regression guard: the
144
+ # point is that the agent must not *break* it. Opting out is explicit so
145
+ # that it is a decision someone made, not a fixture that quietly rotted.
146
+ allow_green_start: bool = False
147
+ # Files the agent never sees: present for the pre-flight, removed (with
148
+ # the pre-flight's caches) for the agent's turn, put back from the fixture
149
+ # for the score. A grader that holds the expected answers — EXPECTED =
150
+ # {"F-201": 5000.00, ...} — or spells out the trap in its docstrings is
151
+ # an answer key when it sits in the workspace. gemini-3.5-flash passed 23
152
+ # of the first 24 tasks with every grader in view.
153
+ hidden: tuple[str, ...] = ()
154
+ # How the agent should go about it, judged on its tool calls and reported
155
+ # beside the outcome without changing it. See evals/behavior.py.
156
+ behavior: tuple[dict, ...] = ()
157
+ # New files the task expects the agent to create that would otherwise be
158
+ # refused as planted: a conftest.py, a pyproject.toml, a module that
159
+ # shares a stdlib name. See _PLANTED_FILES.
160
+ allow_new: tuple[str, ...] = ()
161
+
162
+ @classmethod
163
+ def from_dict(cls, data: dict) -> "TaskSpec":
164
+ missing = [key for key in ("id", "prompt", "verify") if not data.get(key)]
165
+ if missing:
166
+ raise ValueError(f"task is missing required field(s): {', '.join(missing)}")
167
+ return cls(
168
+ id=str(data["id"]),
169
+ prompt=str(data["prompt"]),
170
+ verify=str(data["verify"]),
171
+ fixture=str(data.get("fixture") or ""),
172
+ timeout=int(data.get("timeout") or 300),
173
+ solve_timeout=int(data.get("solve_timeout") or 900),
174
+ tags=tuple(str(t) for t in (data.get("tags") or ())),
175
+ setup=tuple(str(c) for c in (data.get("setup") or ())),
176
+ requires=tuple(str(m) for m in (data.get("requires") or ())),
177
+ protect=(
178
+ tuple(str(g) for g in data["protect"])
179
+ if "protect" in data else cls.protect
180
+ ),
181
+ allow_green_start=bool(data.get("allow_green_start", False)),
182
+ hidden=tuple(str(g) for g in (data.get("hidden") or ())),
183
+ behavior=tuple(_validate_behavior(c) for c in (data.get("behavior") or ())),
184
+ allow_new=tuple(str(g) for g in (data.get("allow_new") or ())),
185
+ )
186
+
187
+
188
+ def _validate_behavior(raw) -> dict:
189
+ from .behavior import validate
190
+
191
+ return validate(raw)
192
+
193
+
194
+ _SECRET = re.compile(
195
+ r"(?i)\b(api[_-]?key|token|password|secret)(\s*[=:]\s*)\S+"
196
+ r"|\b(?:bearer)\s+\S+"
197
+ r"|\b(?:sk|pk|rk)-[A-Za-z0-9_-]{8,}|\bgh[pousr]_[A-Za-z0-9]{12,}|\bAIza[0-9A-Za-z_-]{20,}"
198
+ r"|\bya29\.[0-9A-Za-z_-]+"
199
+ )
200
+
201
+
202
+ def _mask(line: str) -> str:
203
+ def repl(match: re.Match) -> str:
204
+ return f"{match.group(1)}{match.group(2)}***" if match.group(1) else "***"
205
+ return _SECRET.sub(repl, line)
206
+
207
+
208
+ def _log_tail(log: str, lines: int = 23) -> list:
209
+ """The last lines of a task's log, secret-shaped values masked."""
210
+ kept = [line.rstrip() for line in str(log or "").splitlines() if line.strip()]
211
+ return [_mask(line)[:300] for line in kept[-lines:]]
212
+
213
+
214
+ @dataclass(frozen=True)
215
+ class TaskResult:
216
+ task_id: str
217
+ outcome: str
218
+ seconds: float = 0.0
219
+ exit_code: Optional[int] = None
220
+ detail: str = ""
221
+ log: str = ""
222
+ tags: tuple[str, ...] = ()
223
+ # What the agent actually touched. A red check with an empty list is a
224
+ # different failure from a red check after real edits: the first says the
225
+ # agent never engaged, the second says it engaged and got it wrong. Without
226
+ # this the two are indistinguishable in a report, and they call for
227
+ # opposite investigations.
228
+ changed: tuple[str, ...] = ()
229
+ # Behaviour checks on the agent's tool calls: ({check, passed, detail}, …).
230
+ behavior: tuple = ()
231
+ # Tokens the agent reported for this task ({model, provider, input_tokens, …});
232
+ # empty when the solver reports none, which is not the same as zero.
233
+ usage: dict = field(default_factory=dict)
234
+ # The solver ran out of its time budget. Still ERROR (not the model's
235
+ # score), but reported on its own: a suite where a third of the attempts
236
+ # time out has a pass rate over the other two thirds only, and a reader
237
+ # has to be able to see that.
238
+ timed_out: bool = False
239
+
240
+ @property
241
+ def counted(self) -> bool:
242
+ """Whether this result belongs in the pass rate at all."""
243
+ return self.outcome in (PASS, FAIL)
244
+
245
+ def to_dict(self) -> dict:
246
+ return {
247
+ "task_id": self.task_id,
248
+ "outcome": self.outcome,
249
+ "seconds": round(self.seconds, 2),
250
+ "exit_code": self.exit_code,
251
+ "detail": self.detail,
252
+ "tags": list(self.tags),
253
+ "changed": list(self.changed),
254
+ # Why it did not pass. The first CI run's report said only "the
255
+ # agent did not complete (exit 1)"; the reason was in the job log.
256
+ "log_tail": _log_tail(self.log) if self.outcome != PASS else [],
257
+ **({"behavior": [dict(b) for b in self.behavior]} if self.behavior else {}),
258
+ **({"usage": dict(self.usage)} if self.usage else {}),
259
+ **({"timed_out": True} if self.timed_out else {}),
260
+ }
261
+
262
+
263
+ _USAGE_TOKEN_KEYS = ("input_tokens", "output_tokens", "thinking_tokens")
264
+
265
+
266
+ def usage_reported(usage: Optional[dict]) -> bool:
267
+ """Whether a usage record actually reports tokens.
268
+
269
+ An all-zero record is what an agent that never reached a model writes (or
270
+ one whose provider returns no counts); counting it as "reported" made a
271
+ suite's token total look complete while it summed nothing.
272
+ """
273
+ if not usage:
274
+ return False
275
+ for key in _USAGE_TOKEN_KEYS:
276
+ try:
277
+ if int(usage.get(key) or 0) > 0:
278
+ return True
279
+ except (TypeError, ValueError):
280
+ continue
281
+ return False
282
+
283
+
284
+ @dataclass
285
+ class SuiteResult:
286
+ name: str
287
+ results: List[TaskResult] = field(default_factory=list)
288
+
289
+ @property
290
+ def passed(self) -> int:
291
+ return sum(1 for r in self.results if r.outcome == PASS)
292
+
293
+ @property
294
+ def failed(self) -> int:
295
+ return sum(1 for r in self.results if r.outcome == FAIL)
296
+
297
+ @property
298
+ def invalid(self) -> int:
299
+ return sum(1 for r in self.results if r.outcome == INVALID)
300
+
301
+ @property
302
+ def errored(self) -> int:
303
+ return sum(1 for r in self.results if r.outcome == ERROR)
304
+
305
+ @property
306
+ def timed_out(self) -> int:
307
+ """Errored attempts the solver's time budget cut off (a subset of errored)."""
308
+ return sum(1 for r in self.results if r.outcome == ERROR and r.timed_out)
309
+
310
+ @property
311
+ def scored(self) -> int:
312
+ return sum(1 for r in self.results if r.counted)
313
+
314
+ @property
315
+ def pass_rate(self) -> float:
316
+ """Passes over *scored* tasks.
317
+
318
+ Invalid and errored tasks are excluded rather than counted as
319
+ failures. Counting them down would let one broken fixture look like an
320
+ agent regression; counting them up would let it hide one.
321
+ """
322
+ return (self.passed / self.scored) if self.scored else 0.0
323
+
324
+ def usage_totals(self) -> dict:
325
+ """Tokens and seconds summed over tasks whose agent reported usage.
326
+
327
+ ``reported`` says how many did: a total over 12 of 30 tasks is not the
328
+ cost of the suite, and must not be read as one.
329
+ """
330
+ with_usage = [r for r in self.results if usage_reported(r.usage)]
331
+ keys = _USAGE_TOKEN_KEYS
332
+ return {
333
+ "reported": len(with_usage),
334
+ "tasks": len(self.results),
335
+ **{key: sum(int(r.usage.get(key, 0) or 0) for r in with_usage) for key in keys},
336
+ "seconds": round(sum(r.seconds for r in self.results), 2),
337
+ }
338
+
339
+ def by_tag(self) -> dict:
340
+ """Pass rate per tag — the per-industry scoreboard."""
341
+ buckets: dict[str, list[TaskResult]] = {}
342
+ for result in self.results:
343
+ for tag in result.tags:
344
+ buckets.setdefault(tag, []).append(result)
345
+ out = {}
346
+ for tag, results in sorted(buckets.items()):
347
+ scored = [r for r in results if r.counted]
348
+ out[tag] = {
349
+ "passed": sum(1 for r in scored if r.outcome == PASS),
350
+ "scored": len(scored),
351
+ "pass_rate": round(
352
+ sum(1 for r in scored if r.outcome == PASS) / len(scored), 4
353
+ ) if scored else 0.0,
354
+ }
355
+ return out
356
+
357
+ def to_dict(self) -> dict:
358
+ return {
359
+ "suite": self.name,
360
+ "pass_rate": round(self.pass_rate, 4),
361
+ "passed": self.passed,
362
+ "failed": self.failed,
363
+ "invalid": self.invalid,
364
+ "errored": self.errored,
365
+ "timed_out": self.timed_out,
366
+ "scored": self.scored,
367
+ "by_tag": self.by_tag(),
368
+ "usage": self.usage_totals(),
369
+ "per_task": self.per_task(),
370
+ **({"behavior": self.behavior()} if any(r.behavior for r in self.results) else {}),
371
+ "results": [r.to_dict() for r in self.results],
372
+ }
373
+
374
+ def behavior(self) -> dict:
375
+ """Passes over runs for each behaviour check, keyed ``task: check``."""
376
+ out: dict = {}
377
+ for result in self.results:
378
+ for item in result.behavior:
379
+ stats = out.setdefault(f"{result.task_id}: {item['check']}", {"passed": 0, "runs": 0})
380
+ stats["runs"] += 1
381
+ stats["passed"] += bool(item["passed"])
382
+ return out
383
+
384
+ def merge(self, other: "SuiteResult") -> None:
385
+ """Fold another run of the same suite into this one."""
386
+ self.results.extend(other.results)
387
+
388
+ def per_task(self) -> dict:
389
+ """Passes over attempts for each task id, across every repeat.
390
+
391
+ A single sample is not a score. Observed on the first real run: one
392
+ task passed alone and failed in the same suite minutes later, from the
393
+ same model. Reporting 2/5 without saying how many attempts produced it
394
+ invites reading noise as a regression.
395
+ """
396
+ buckets: dict[str, list[TaskResult]] = {}
397
+ for result in self.results:
398
+ buckets.setdefault(result.task_id, []).append(result)
399
+ out = {}
400
+ for task_id, results in buckets.items():
401
+ scored = [r for r in results if r.counted]
402
+ out[task_id] = {
403
+ "passed": sum(1 for r in scored if r.outcome == PASS),
404
+ "attempts": len(scored),
405
+ "outcomes": [r.outcome for r in results],
406
+ }
407
+ return out
408
+
409
+ def summary_line(self) -> str:
410
+ parts = [f"{self.name}: {self.passed}/{self.scored} ({self.pass_rate:.0%})"]
411
+ if self.invalid:
412
+ parts.append(f"{self.invalid} invalid")
413
+ if self.errored:
414
+ parts.append(f"{self.errored} error")
415
+ if self.timed_out:
416
+ parts.append(f"{self.timed_out} timed out (of the errors, unscored)")
417
+ checks = [item for r in self.results for item in r.behavior]
418
+ if checks:
419
+ parts.append(f"behaviour {sum(bool(i['passed']) for i in checks)}/{len(checks)}")
420
+ return " · ".join(parts)
421
+
422
+
423
+ # ``(prompt, workspace) -> anything``. Injected so the harness can be tested,
424
+ # and so the same suite can score a different agent — the CLI, a subagent
425
+ # backend, or a competitor — without the suite knowing which.
426
+ Solver = Callable[[str, Path], Any]
427
+
428
+
429
+ def _resolve(command: str) -> str:
430
+ """Substitute ``{python}`` with the interpreter running the harness.
431
+
432
+ Without this a suite silently verifies against whatever ``python3`` means
433
+ on PATH, which is how five tasks once reported red because that
434
+ interpreter had no pytest installed.
435
+
436
+ The path is quoted, because the command runs through a shell. Unquoted, an
437
+ interpreter under "Application Support" — where the macOS app keeps its
438
+ projects — split at the space, every check exited 126, and the pre-flight
439
+ reported all four tasks red: measuring nothing while saying it was fine.
440
+ """
441
+ if os.name == "nt":
442
+ interpreter = subprocess.list2cmdline([sys.executable])
443
+ else:
444
+ interpreter = shlex.quote(sys.executable)
445
+ return (command or "").replace("{python}", interpreter)
446
+
447
+
448
+ # Exit codes with which the shell says it never ran the check at all: 125 is
449
+ # _run's own "could not start", 126 not executable, 127 not found, 9009 the
450
+ # Windows not-found. A red result for any of these is not a broken fixture and
451
+ # not a failed agent — counting it as either lets an environment problem pose
452
+ # as a measurement.
453
+ _COULD_NOT_RUN = {125: "could not start", 126: "not executable", 127: "command not found",
454
+ 9009: "command not found"}
455
+
456
+ # The code alone is not evidence: the check runs the agent's code, and that
457
+ # code can `os._exit(127)` to turn a FAIL into an unscored ERROR. Output is
458
+ # not evidence either, since the same code can print "command not found". So:
459
+ #
460
+ # * 125 counts only with _run's own start-failure marker, which carries a
461
+ # per-process nonce the check cannot know;
462
+ # * 126/127/9009 count only when a program the command line names really
463
+ # cannot be found or executed from the task's workspace. When every program
464
+ # resolves, the code came from inside the check, and a red check is a FAIL.
465
+ _START_FAILURE = f"could not run command [{secrets.token_hex(8)}]: "
466
+ _SHELL_BUILTINS = frozenset({
467
+ "cd", "export", "set", "unset", "true", "false", ":", "test", "[", "echo",
468
+ "exit", "source", ".", "eval", "exec", "env", "command", "builtin", "!",
469
+ })
470
+ _SEPARATORS = frozenset({"&&", "||", ";", "|", "&"})
471
+
472
+
473
+ def _missing_program(command: str, cwd: Optional[Path]) -> bool:
474
+ """True when some program the command line runs cannot be found or executed."""
475
+ try:
476
+ tokens = shlex.split(_resolve(command), posix=os.name != "nt")
477
+ except ValueError:
478
+ return True # cannot parse it: do not claim it ran
479
+ starts = [0] + [i + 1 for i, tok in enumerate(tokens) if tok in _SEPARATORS]
480
+ for i in starts:
481
+ while i < len(tokens) and re.match(r"^[A-Za-z_][A-Za-z0-9_]*=", tokens[i]):
482
+ i += 1 # FOO=bar cmd
483
+ if i >= len(tokens):
484
+ continue
485
+ program = tokens[i].strip("\"'")
486
+ if not program or program in _SHELL_BUILTINS:
487
+ continue
488
+ if "/" in program or (os.name == "nt" and "\\" in program):
489
+ path = Path(program)
490
+ if not path.is_absolute() and cwd is not None:
491
+ path = Path(cwd) / path
492
+ if not (path.is_file() and os.access(path, os.X_OK)):
493
+ return True
494
+ elif shutil.which(program) is None:
495
+ return True
496
+ return False
497
+
498
+
499
+ def _could_not_run(code: int, log: str, command: str = "", cwd: Optional[Path] = None) -> str:
500
+ reason = _COULD_NOT_RUN.get(code)
501
+ if reason is None:
502
+ return ""
503
+ text = log or ""
504
+ if code == 125:
505
+ if _START_FAILURE not in text:
506
+ return ""
507
+ elif not _missing_program(command, cwd):
508
+ return ""
509
+ last = text.strip().splitlines()[-1:] or [""]
510
+ return f"the check itself could not run (exit {code}, {reason}): {last[0]}".rstrip(": ")
511
+
512
+
513
+ def _missing_modules(names: Iterable[str]) -> list[str]:
514
+ missing = []
515
+ for name in names:
516
+ try:
517
+ if importlib.util.find_spec(name) is None:
518
+ missing.append(name)
519
+ except (ImportError, ValueError):
520
+ missing.append(name)
521
+ return missing
522
+
523
+
524
+ def _matches(rel: str, globs: Iterable[str]) -> bool:
525
+ import fnmatch
526
+
527
+ name = rel.replace("\\", "/")
528
+ base = name.rsplit("/", 1)[-1]
529
+ return any(fnmatch.fnmatch(name, g) or fnmatch.fnmatch(base, g) for g in globs)
530
+
531
+
532
+ def _violates_protection(changed: Iterable[str], patterns: Iterable[str]) -> tuple[str, ...]:
533
+ """Protected files the agent modified."""
534
+ globs = tuple(patterns)
535
+ return tuple(rel for rel in changed if _matches(rel, globs))
536
+
537
+
538
+ # Files that change how the check itself runs rather than what it checks. A
539
+ # conftest.py can rewrite every test outcome, a pytest.ini/pyproject.toml/
540
+ # setup.cfg/tox.ini can deselect the failing tests or add plugins, a .pth or
541
+ # sitecustomize.py runs at interpreter start-up, and a pytest.py can stand in
542
+ # for pytest. Protection covers only files that were there before the agent's
543
+ # turn, so an agent could pass any task by *adding* one of these. They are
544
+ # refused unless the task lists them in ``allow_new``.
545
+ _PLANTED_FILES = (
546
+ "conftest.py", "pytest.ini", "pyproject.toml", "setup.cfg", "tox.ini",
547
+ "sitecustomize.py", "usercustomize.py", "*.pth",
548
+ )
549
+ # Modules the check imports. A new file or package with one of these names
550
+ # shadows the real one; so does any stdlib name.
551
+ _CHECK_MODULES = frozenset({"pytest", "_pytest", "pluggy", "py", "iniconfig", "unittest"})
552
+
553
+
554
+ def _shadowed_module(rel: str) -> str:
555
+ """The importable name *rel* would shadow, or "" when it shadows nothing."""
556
+ parts = rel.replace("\\", "/").split("/")
557
+ name = parts[-1]
558
+ if name in ("__init__.py", "__init__.pyc") and len(parts) > 1:
559
+ module = parts[-2]
560
+ elif name.endswith((".py", ".pyc")) and "." not in name.rsplit(".", 1)[0]:
561
+ module = name.rsplit(".", 1)[0]
562
+ else:
563
+ return ""
564
+ stdlib = getattr(sys, "stdlib_module_names", frozenset())
565
+ return module if module in _CHECK_MODULES or module in stdlib else ""
566
+
567
+
568
+ def _planted(added: Iterable[str], allowed: Iterable[str]) -> tuple[str, ...]:
569
+ """New files that could change how the check runs, minus what the task allows."""
570
+ allow = tuple(allowed)
571
+ out = []
572
+ for rel in added:
573
+ if allow and _matches(rel, allow):
574
+ continue
575
+ if _matches(rel, _PLANTED_FILES) or _shadowed_module(rel):
576
+ out.append(rel)
577
+ return tuple(out)
578
+
579
+
580
+ def _hide(workspace: Path, globs: Sequence[str]) -> tuple[str, ...]:
581
+ """Remove the hidden files, and every cache the pre-flight left, from *workspace*.
582
+
583
+ The caches matter as much as the files: .pytest_cache records the failing
584
+ tests by name and __pycache__ holds the grader compiled, docstrings and
585
+ expected values included.
586
+ """
587
+ if not globs:
588
+ return ()
589
+ hidden = tuple(
590
+ str(path.relative_to(workspace)) for path in sorted(workspace.rglob("*"))
591
+ if path.is_file() and _matches(str(path.relative_to(workspace)), globs)
592
+ )
593
+ for rel in hidden:
594
+ (workspace / rel).unlink()
595
+ for cache in sorted(workspace.rglob("*"), reverse=True):
596
+ if cache.is_dir() and cache.name in ("__pycache__", ".pytest_cache"):
597
+ shutil.rmtree(cache, ignore_errors=True)
598
+ return hidden
599
+
600
+
601
+ def _restore(hidden: Sequence[str], source: Path, workspace: Path) -> None:
602
+ """Put the hidden files back from the fixture, over anything the agent wrote there."""
603
+ for rel in hidden:
604
+ target = workspace / rel
605
+ target.parent.mkdir(parents=True, exist_ok=True)
606
+ shutil.copy2(source / rel, target)
607
+
608
+
609
+ def _snapshot(root: Path) -> dict[str, str]:
610
+ """Content digests for every file in *root*, keyed by relative path."""
611
+ import hashlib
612
+
613
+ out: dict[str, str] = {}
614
+ for path in sorted(root.rglob("*")):
615
+ if not path.is_file():
616
+ continue
617
+ rel = str(path.relative_to(root))
618
+ # Residue the *check* writes, not the agent's work. Reporting it makes
619
+ # every task look like it touched a dozen files and buries the one edit
620
+ # that matters.
621
+ if any(part in _RUN_RESIDUE for part in rel.replace("\\", "/").split("/")):
622
+ continue
623
+ try:
624
+ out[rel] = hashlib.sha256(path.read_bytes()).hexdigest()
625
+ except OSError:
626
+ continue
627
+ return out
628
+
629
+
630
+ def _changed_paths(before: dict[str, str], after: dict[str, str]) -> tuple[str, ...]:
631
+ """Files the agent created, edited, or deleted."""
632
+ touched = [
633
+ rel for rel in sorted(set(before) | set(after))
634
+ if before.get(rel) != after.get(rel)
635
+ ]
636
+ return tuple(touched)
637
+
638
+
639
+ # How an agent's turn can end when the agent itself was running: these are about the
640
+ # work, so the task's check decides. Anything else in `stop_reason` is the transport
641
+ # or the provider failing (an HTTP error text, a crash), which is not the model's score.
642
+ _AGENT_STOPS = frozenset({"completed", "checks_failed", "max_rounds", "budget_exhausted", "loop_guard"})
643
+
644
+
645
+ def _agent_finished(stdout: str) -> bool:
646
+ """True when the agent's event stream ends with a turn.completed that ran to an agent-level stop."""
647
+ last = None
648
+ for line in stdout.splitlines():
649
+ if line.startswith("{") and '"turn.completed"' in line:
650
+ try:
651
+ last = json.loads(line)
652
+ except ValueError:
653
+ continue
654
+ return bool(last) and str(last.get("stop_reason") or "") in _AGENT_STOPS
655
+
656
+
657
+ def _solver_failure(outcome: Any) -> Optional[tuple[int, str]]:
658
+ """``(exit_code, log)`` when the solver reports it did not complete.
659
+
660
+ Duck-typed on ``returncode`` so a plain callable that returns nothing —
661
+ every in-process solver, and the check-only one — is unaffected, while a
662
+ ``subprocess.CompletedProcess`` from the real agent is inspected.
663
+ """
664
+ if outcome is None:
665
+ return None
666
+ code = getattr(outcome, "returncode", None)
667
+ if not isinstance(code, int) or code == 0:
668
+ return None
669
+ if _agent_finished(str(getattr(outcome, "stdout", "") or "")):
670
+ # The agent ran its turn and said how it ended; the non-zero exit is its verdict on
671
+ # its own work (its acceptance checks were red), not a crash. The check scores it.
672
+ return None
673
+ log = "\n".join(
674
+ part for part in (
675
+ str(getattr(outcome, "stderr", "") or ""),
676
+ str(getattr(outcome, "stdout", "") or ""),
677
+ ) if part.strip()
678
+ )
679
+ return code, log
680
+
681
+
682
+ def _run(command: str, cwd: Path, timeout: int) -> tuple[int, str]:
683
+ command = _resolve(command)
684
+ try:
685
+ proc = subprocess.run(
686
+ command,
687
+ shell=True,
688
+ cwd=str(cwd),
689
+ capture_output=True,
690
+ text=True,
691
+ timeout=timeout,
692
+ check=False,
693
+ )
694
+ except subprocess.TimeoutExpired:
695
+ # A check that never finishes is a failed check. Reporting it as
696
+ # anything else would let an infinite loop score as a pass.
697
+ return 124, f"verification timed out after {timeout}s"
698
+ except Exception as exc:
699
+ return 125, f"{_START_FAILURE}{exc}"
700
+ output = "\n".join(part for part in (proc.stdout, proc.stderr) if part.strip())
701
+ return proc.returncode, output
702
+
703
+
704
+ def load_suite(path: str | Path) -> tuple[str, list[TaskSpec]]:
705
+ """Read a YAML suite file into task specs."""
706
+ import yaml
707
+
708
+ file_path = Path(path).expanduser().resolve()
709
+ data = yaml.safe_load(file_path.read_text(encoding="utf-8")) or {}
710
+ name = str(data.get("suite") or file_path.stem)
711
+ tasks = [TaskSpec.from_dict(entry) for entry in (data.get("tasks") or [])]
712
+
713
+ duplicates = {t.id for t in tasks if [x.id for x in tasks].count(t.id) > 1}
714
+ if duplicates:
715
+ raise ValueError(f"duplicate task id(s) in {file_path.name}: {', '.join(sorted(duplicates))}")
716
+ return name, tasks
717
+
718
+
719
+ def _prepare_workspace(task: TaskSpec, fixtures_root: Path, scratch: Path) -> Path:
720
+ workspace = scratch / task.id
721
+ if workspace.exists():
722
+ shutil.rmtree(workspace)
723
+ if task.fixture:
724
+ source = fixtures_root / task.fixture
725
+ if not source.is_dir():
726
+ raise FileNotFoundError(f"fixture not found: {source}")
727
+ # Never copy build residue. A __pycache__ carried over from a run of
728
+ # the fixture in place holds bytecode for the *broken* source, and
729
+ # Python will happily import it — so a task could score red after a
730
+ # correct fix, or green before one.
731
+ shutil.copytree(source, workspace, ignore=_IGNORED_FIXTURE_ENTRIES)
732
+ else:
733
+ workspace.mkdir(parents=True)
734
+ return workspace
735
+
736
+
737
+ def run_task(
738
+ task: TaskSpec,
739
+ *,
740
+ solver: Solver,
741
+ fixtures_root: str | Path,
742
+ scratch_root: str | Path | None = None,
743
+ keep_workspace: bool = False,
744
+ ) -> TaskResult:
745
+ """Prepare, pre-check, solve, and score one task."""
746
+ started = time.time()
747
+ fixtures = Path(fixtures_root).expanduser().resolve()
748
+ scratch = Path(scratch_root).expanduser().resolve() if scratch_root else Path(
749
+ tempfile.mkdtemp(prefix="aria-eval-")
750
+ )
751
+ scratch.mkdir(parents=True, exist_ok=True)
752
+
753
+ behaviour: list = [] # filled once the agent has had its turn
754
+ reported: dict = {} # the agent's own usage report, once it has had its turn
755
+
756
+ def _result(outcome: str, **kwargs) -> TaskResult:
757
+ return TaskResult(
758
+ task_id=task.id, outcome=outcome,
759
+ seconds=time.time() - started, tags=task.tags, behavior=tuple(behaviour),
760
+ usage=dict(reported), **kwargs,
761
+ )
762
+
763
+ # Everything below runs inside this try so the scratch directory is removed
764
+ # on EVERY path, not just the happy one.
765
+ #
766
+ # It used to open after the setup checks, and the two error returns above
767
+ # it — a missing fixture, a missing dependency — leaked one temp directory
768
+ # each. That is once per error path per run, and the test suite exercises
769
+ # both deliberately, so a full test run leaked two directories every time.
770
+ # 57 of them had accumulated in $TMPDIR before anyone noticed, which is the
771
+ # shape of this class of bug: too small to be felt, never self-correcting.
772
+ try:
773
+ try:
774
+ workspace = _prepare_workspace(task, fixtures, scratch)
775
+ except Exception as exc:
776
+ return _result(ERROR, detail=f"workspace setup failed: {exc}")
777
+
778
+ missing = _missing_modules(task.requires)
779
+ if missing:
780
+ return _result(
781
+ ERROR,
782
+ detail=f"environment is missing {', '.join(missing)} — the check cannot run",
783
+ )
784
+
785
+ for command in task.setup:
786
+ code, output = _run(command, workspace, task.timeout)
787
+ if code != 0:
788
+ return _result(ERROR, exit_code=code,
789
+ detail=f"setup command failed: {command}",
790
+ log=_trim(output))
791
+
792
+ # ── the pre-flight ────────────────────────────────────────────────
793
+ # Confirm the task is actually broken before asking anyone to fix it.
794
+ before_code, before_log = _run(task.verify, workspace, task.timeout)
795
+ broken = _could_not_run(before_code, before_log, task.verify, workspace)
796
+ if broken:
797
+ return _result(ERROR, exit_code=before_code, detail=broken, log=_trim(before_log))
798
+ if before_code == 0 and not task.allow_green_start:
799
+ return _result(
800
+ INVALID, exit_code=0,
801
+ detail="fixture already passes its own check — this task measures nothing",
802
+ log=_trim(before_log),
803
+ )
804
+
805
+ # ── the agent's turn ──────────────────────────────────────────────
806
+ hidden = _hide(workspace, task.hidden)
807
+ before = _snapshot(workspace)
808
+ try:
809
+ outcome = solver(task.prompt, workspace)
810
+ except Exception as exc:
811
+ # A solver crash is not the agent failing the task; scoring it as
812
+ # a fail would blame the model for a harness or provider outage.
813
+ return _result(ERROR, detail=f"solver raised: {exc}")
814
+ if isinstance(getattr(outcome, "usage", None), dict):
815
+ reported.update(outcome.usage)
816
+ # Only when the solver ran an agent: the pre-flight solver returns
817
+ # nothing, and "write_file was never called" would read as a pass.
818
+ if task.behavior and outcome is not None:
819
+ from .behavior import evaluate
820
+
821
+ behaviour.extend(evaluate(task.behavior, str(getattr(outcome, "stdout", "") or "")))
822
+
823
+ # ── the score ─────────────────────────────────────────────────────
824
+ # The check runs no matter what the solver reported, because the check
825
+ # is the source of truth and the solver's exit code is not. A run that
826
+ # edited the file correctly and then exited 1 on an empty final message
827
+ # is a PASS: the work is on disk and the tests are green.
828
+ changed = _changed_paths(before, _snapshot(workspace))
829
+ if hidden:
830
+ _restore(hidden, fixtures / task.fixture, workspace)
831
+
832
+ # Checked before the verdict, and it overrides a green check. Observed
833
+ # in a real run: an agent edited test_settlement.py rather than the
834
+ # module under test. The suite would have called that a PASS and the
835
+ # number would have been a lie.
836
+ #
837
+ # Only files that were there for the agent to tamper with: a test file
838
+ # it writes for itself is ordinary work, and with hidden graders the
839
+ # tests it is told not to edit are not there at all.
840
+ tampered = _violates_protection([rel for rel in changed if rel in before], task.protect)
841
+ if tampered:
842
+ return _result(
843
+ FAIL,
844
+ detail=f"modified protected file(s): {', '.join(tampered)}",
845
+ changed=changed,
846
+ )
847
+ # ...but a file it *adds* can still change how the check runs.
848
+ added = [rel for rel in changed if rel not in before and rel not in hidden
849
+ and (workspace / rel).is_file()]
850
+ planted = _planted(added, task.allow_new)
851
+ if planted:
852
+ return _result(
853
+ FAIL,
854
+ detail=(f"added file(s) that change how the check runs: {', '.join(planted)} "
855
+ "(declare expected ones in the task's allow_new)"),
856
+ changed=changed,
857
+ )
858
+
859
+ after_code, after_log = _run(task.verify, workspace, task.timeout)
860
+ if after_code == 0:
861
+ return _result(PASS, exit_code=0, changed=changed)
862
+ broken = _could_not_run(after_code, after_log, task.verify, workspace)
863
+ if broken:
864
+ return _result(ERROR, exit_code=after_code, detail=broken,
865
+ log=_trim(after_log), changed=changed)
866
+
867
+ # Red — now the solver's status decides who is to blame. A subprocess
868
+ # that died in seconds on a provider outage or a crash never gave the
869
+ # agent its turn, and scoring that as FAIL makes an infrastructure
870
+ # problem look like an incapable model. That is exactly the confusion
871
+ # this harness separates PASS/FAIL from ERROR to avoid.
872
+ failure = _solver_failure(outcome)
873
+ if failure is not None:
874
+ code, log = failure
875
+ timed_out = bool(getattr(outcome, "timed_out", False))
876
+ return _result(
877
+ ERROR, exit_code=code,
878
+ detail=(f"the agent ran out of time (exit {code}); check still red" if timed_out
879
+ else f"the agent did not complete (exit {code}); check still red"),
880
+ log=_trim(log), changed=changed, timed_out=timed_out,
881
+ )
882
+
883
+ detail = f"`{task.verify}` exited {after_code}"
884
+ if not changed:
885
+ detail += " — the agent changed nothing"
886
+ # The check's output says what is wrong; the agent's last words say
887
+ # what it thought it did. inventory-reorder failed twice on "reorder.json
888
+ # was not written" with nothing to show whether the agent computed the
889
+ # policy and only reported it, or never got that far.
890
+ agent_said = "\n".join(
891
+ part for part in (str(getattr(outcome, "stdout", "") or ""), str(getattr(outcome, "stderr", "") or ""))
892
+ if part.strip()
893
+ )
894
+ agent_tail = "\n".join([line for line in agent_said.splitlines() if line.strip()][-8:])
895
+ log = _trim(after_log)
896
+ if agent_tail:
897
+ log = f"{log}\n--- agent, last lines ---\n{agent_tail}"
898
+ return _result(
899
+ FAIL, exit_code=after_code, detail=detail,
900
+ log=log, changed=changed,
901
+ )
902
+ finally:
903
+ if not keep_workspace and scratch_root is None:
904
+ shutil.rmtree(scratch, ignore_errors=True)
905
+
906
+
907
+ def run_suite(
908
+ tasks: Sequence[TaskSpec],
909
+ *,
910
+ solver: Solver,
911
+ fixtures_root: str | Path,
912
+ name: str = "suite",
913
+ scratch_root: str | Path | None = None,
914
+ only: Iterable[str] = (),
915
+ tags: Iterable[str] = (),
916
+ on_result: Optional[Callable[[TaskResult], None]] = None,
917
+ keep_workspace: bool = False,
918
+ ) -> SuiteResult:
919
+ """Run every task and collect the scoreboard."""
920
+ wanted_ids = {str(i) for i in only if i}
921
+ wanted_tags = {str(t) for t in tags if t}
922
+
923
+ selected = [
924
+ task for task in tasks
925
+ if (not wanted_ids or task.id in wanted_ids)
926
+ and (not wanted_tags or wanted_tags & set(task.tags))
927
+ ]
928
+
929
+ suite = SuiteResult(name=name)
930
+ for task in selected:
931
+ result = run_task(
932
+ task,
933
+ solver=solver,
934
+ fixtures_root=fixtures_root,
935
+ scratch_root=scratch_root,
936
+ keep_workspace=keep_workspace,
937
+ )
938
+ suite.results.append(result)
939
+ if on_result is not None:
940
+ on_result(result)
941
+ return suite
942
+
943
+
944
+ def write_report(suite: SuiteResult, path: str | Path) -> Path:
945
+ """Persist the scoreboard as JSON so runs can be compared over time."""
946
+ out = Path(path).expanduser().resolve()
947
+ out.parent.mkdir(parents=True, exist_ok=True)
948
+ out.write_text(json.dumps(suite.to_dict(), ensure_ascii=False, indent=2), encoding="utf-8")
949
+ return out