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,1344 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ import shutil
6
+ import subprocess
7
+ import tempfile
8
+ from collections import defaultdict
9
+ from collections.abc import Iterable, Iterator, Mapping, Sequence
10
+ from contextlib import contextmanager
11
+ from datetime import UTC, datetime
12
+ from pathlib import Path
13
+ from typing import Any, Protocol
14
+
15
+ from .discovery import project_skill_root_relative_paths
16
+ from .eval_models import (
17
+ SkillsEvalArtifacts,
18
+ SkillsEvalCase,
19
+ SkillsEvalExecutionRequest,
20
+ SkillsEvalExecutionResult,
21
+ SkillsEvalMode,
22
+ SkillsEvalRecord,
23
+ SkillsEvalVerificationResult,
24
+ )
25
+
26
+ _CONVENTION_FILENAMES = {"AGENTS.md", "CLAUDE.md", "CONVENTIONS.md"}
27
+ _WORKSPACE_COPY_IGNORE_NAMES = {
28
+ ".git",
29
+ "__pycache__",
30
+ ".pytest_cache",
31
+ ".ruff_cache",
32
+ ".mypy_cache",
33
+ ".venv",
34
+ ".alysis",
35
+ "artifacts",
36
+ "build",
37
+ "dist",
38
+ "node_modules",
39
+ "reports",
40
+ "worktrees",
41
+ }
42
+ DEFAULT_SKILLS_EVAL_MODES: tuple[SkillsEvalMode, ...] = (
43
+ SkillsEvalMode(
44
+ name="baseline",
45
+ conventions_enabled=False,
46
+ skills_enabled=False,
47
+ skills_auto_invoke=False,
48
+ ),
49
+ SkillsEvalMode(
50
+ name="conventions_only",
51
+ conventions_enabled=True,
52
+ skills_enabled=False,
53
+ skills_auto_invoke=False,
54
+ ),
55
+ SkillsEvalMode(
56
+ name="skills_manual_only",
57
+ conventions_enabled=False,
58
+ skills_enabled=True,
59
+ skills_auto_invoke=False,
60
+ ),
61
+ SkillsEvalMode(
62
+ name="skills_auto_only",
63
+ conventions_enabled=False,
64
+ skills_enabled=True,
65
+ skills_auto_invoke=True,
66
+ ),
67
+ SkillsEvalMode(
68
+ name="combined_manual",
69
+ conventions_enabled=True,
70
+ skills_enabled=True,
71
+ skills_auto_invoke=False,
72
+ ),
73
+ SkillsEvalMode(
74
+ name="combined_auto",
75
+ conventions_enabled=True,
76
+ skills_enabled=True,
77
+ skills_auto_invoke=True,
78
+ ),
79
+ )
80
+ _SKILL_LIFECYCLE_COMMAND_PATTERN = re.compile(
81
+ r"\balysis\s+skill\s+"
82
+ r"(init|create|validate|install|enable|disable|remove|uninstall)\b"
83
+ )
84
+ _COMPLETION_GATE_FAILURE_EVENTS = {
85
+ "one_shot_completion_gate_failed",
86
+ "interactive_completion_gate_failed",
87
+ }
88
+ _COMPLETION_GATE_INCOMPLETE_EVENTS = {
89
+ "one_shot_completion_gate_incomplete_after_retries",
90
+ "interactive_completion_gate_incomplete_after_retries",
91
+ }
92
+ _VERIFICATION_CREDIT_MISS_STAGES = {
93
+ "verification_not_attempted",
94
+ "verification_incomplete",
95
+ }
96
+ _FORCED_FINAL_SUMMARY_EVENT = "forced_final_summary_requested"
97
+ DEFAULT_SKILLS_LAUNCH_GATES: dict[str, float] = {
98
+ "pass_rate_min": 0.80,
99
+ "completion_gate_failure_rate_max": 0.05,
100
+ "completion_gate_incomplete_after_retries_rate_max": 0.01,
101
+ "forced_final_summary_rate_max": 0.01,
102
+ "verification_credit_miss_rate_max": 0.05,
103
+ "relevant_skill_usage_rate_min": 0.90,
104
+ "explicit_skill_success_rate_min": 1.0,
105
+ }
106
+ _SKILLS_EVAL_MODE_BY_NAME = {mode.name: mode for mode in DEFAULT_SKILLS_EVAL_MODES}
107
+ _MANUAL_LAUNCH_MODE_NAMES = ("skills_manual_only", "combined_manual")
108
+ _AUTO_LAUNCH_MODE_NAMES = ("skills_auto_only", "combined_auto")
109
+
110
+
111
+ class SkillsEvalExecutor(Protocol):
112
+ def execute(self, request: SkillsEvalExecutionRequest) -> SkillsEvalExecutionResult: ...
113
+
114
+
115
+ class SkillsEvalVerificationRunner(Protocol):
116
+ def __call__(self, *, workspace: Path, command: str) -> SkillsEvalVerificationResult: ...
117
+
118
+
119
+ def load_skills_eval_cases(manifest_path: Path) -> tuple[SkillsEvalCase, ...]:
120
+ resolved_manifest = manifest_path.expanduser().resolve()
121
+ raw = json.loads(resolved_manifest.read_text(encoding="utf-8"))
122
+ if isinstance(raw, dict):
123
+ raw_cases = raw.get("cases")
124
+ else:
125
+ raw_cases = raw
126
+ if not isinstance(raw_cases, list):
127
+ raise ValueError(
128
+ "skills eval manifest must be a JSON array or an object with a 'cases' list"
129
+ )
130
+
131
+ cases: list[SkillsEvalCase] = []
132
+ seen_ids: set[str] = set()
133
+ for idx, item in enumerate(raw_cases):
134
+ if not isinstance(item, dict):
135
+ raise ValueError(f"skills eval case #{idx + 1} must be a JSON object")
136
+ case_id = str(item.get("id") or "").strip()
137
+ if not case_id:
138
+ raise ValueError(f"skills eval case #{idx + 1} is missing a non-empty 'id'")
139
+ lowered = case_id.casefold()
140
+ if lowered in seen_ids:
141
+ raise ValueError(f"duplicate skills eval case id: {case_id}")
142
+ seen_ids.add(lowered)
143
+
144
+ workspace_raw = str(item.get("workspace") or "").strip()
145
+ if not workspace_raw:
146
+ raise ValueError(f"skills eval case '{case_id}' is missing 'workspace'")
147
+ workspace = (resolved_manifest.parent / workspace_raw).resolve()
148
+
149
+ task = str(item.get("task") or "").strip()
150
+ if not task:
151
+ raise ValueError(f"skills eval case '{case_id}' is missing 'task'")
152
+
153
+ invocation_mode = str(item.get("invocation_mode") or "normal").strip()
154
+ if invocation_mode not in {"normal", "explicit_skill"}:
155
+ raise ValueError(
156
+ f"skills eval case '{case_id}' has unsupported invocation_mode: {invocation_mode}"
157
+ )
158
+ explicit_skill_name = str(item.get("explicit_skill_name") or "").strip() or None
159
+ if invocation_mode == "explicit_skill" and not explicit_skill_name:
160
+ raise ValueError(
161
+ f"skills eval case '{case_id}' requires 'explicit_skill_name' for explicit_skill mode"
162
+ )
163
+ if invocation_mode == "normal":
164
+ explicit_skill_name = None
165
+
166
+ cases.append(
167
+ SkillsEvalCase(
168
+ id=case_id,
169
+ workspace=workspace,
170
+ task=task,
171
+ invocation_mode=invocation_mode,
172
+ explicit_skill_name=explicit_skill_name,
173
+ expected_skills=_normalized_string_tuple(item.get("expected_skills")),
174
+ verification_command=_normalized_optional_string(item.get("verification_command")),
175
+ tags=_normalized_string_tuple(item.get("tags")),
176
+ notes=str(item.get("notes") or "").strip(),
177
+ )
178
+ )
179
+
180
+ return tuple(cases)
181
+
182
+
183
+ def resolve_skills_eval_modes(
184
+ selected_names: Sequence[str] | None = None,
185
+ ) -> tuple[SkillsEvalMode, ...]:
186
+ if not selected_names:
187
+ return DEFAULT_SKILLS_EVAL_MODES
188
+ requested = [str(name or "").strip() for name in selected_names if str(name or "").strip()]
189
+ if not requested:
190
+ return DEFAULT_SKILLS_EVAL_MODES
191
+ available = {mode.name: mode for mode in DEFAULT_SKILLS_EVAL_MODES}
192
+ resolved: list[SkillsEvalMode] = []
193
+ seen: set[str] = set()
194
+ for name in requested:
195
+ if name not in available:
196
+ valid = ", ".join(mode.name for mode in DEFAULT_SKILLS_EVAL_MODES)
197
+ raise ValueError(f"unknown skills eval mode '{name}' (expected one of: {valid})")
198
+ if name in seen:
199
+ continue
200
+ seen.add(name)
201
+ resolved.append(available[name])
202
+ return tuple(resolved)
203
+
204
+
205
+ @contextmanager
206
+ def prepared_skills_eval_workspace(
207
+ *,
208
+ source_workspace: Path,
209
+ mode: SkillsEvalMode,
210
+ temp_base_dir: Path | None = None,
211
+ ) -> Iterator[Path]:
212
+ source = source_workspace.expanduser().resolve()
213
+ if not source.exists() or not source.is_dir():
214
+ raise FileNotFoundError(f"skills eval workspace does not exist: {source}")
215
+ temp_root_base = temp_base_dir.resolve() if temp_base_dir is not None else None
216
+ if temp_root_base is not None:
217
+ temp_root_base.mkdir(parents=True, exist_ok=True)
218
+ with tempfile.TemporaryDirectory(
219
+ prefix=_skills_eval_tempdir_prefix(mode.name),
220
+ dir=temp_root_base,
221
+ ) as temp_dir:
222
+ temp_root = Path(temp_dir)
223
+ run_workspace = temp_root / "workspace"
224
+ shutil.copytree(
225
+ source,
226
+ run_workspace,
227
+ ignore=shutil.ignore_patterns(*sorted(_WORKSPACE_COPY_IGNORE_NAMES)),
228
+ )
229
+ if not mode.conventions_enabled:
230
+ _mask_conventions_in_workspace(run_workspace)
231
+ if not mode.skills_enabled:
232
+ _mask_project_skill_roots_in_workspace(run_workspace)
233
+ yield run_workspace
234
+
235
+
236
+ def extract_skills_eval_metrics(
237
+ events: Sequence[Mapping[str, Any]] | Iterable[Mapping[str, Any]],
238
+ *,
239
+ workspace_root: Path | None = None,
240
+ ) -> dict[str, object]:
241
+ matched_skill_names: list[str] = []
242
+ skill_read_names: list[str] = []
243
+ lifecycle_cli_commands: list[str] = []
244
+ manual_skill_bundle_names: list[str] = []
245
+ manual_skill_bundle_access_count = 0
246
+ tool_call_count = 0
247
+ completion_gate_failure_count = 0
248
+ completion_gate_incomplete_after_retries_count = 0
249
+ forced_final_summary_count = 0
250
+ verification_credit_miss_count = 0
251
+
252
+ for event in events:
253
+ event_type = str(event.get("type") or "")
254
+ payload_obj = event.get("payload")
255
+ payload = payload_obj if isinstance(payload_obj, Mapping) else {}
256
+ if event_type in _COMPLETION_GATE_FAILURE_EVENTS:
257
+ completion_gate_failure_count += 1
258
+ elif event_type in _COMPLETION_GATE_INCOMPLETE_EVENTS:
259
+ completion_gate_incomplete_after_retries_count += 1
260
+ elif event_type == _FORCED_FINAL_SUMMARY_EVENT:
261
+ forced_final_summary_count += 1
262
+ if _event_is_verification_credit_miss(event_type=event_type, payload=payload):
263
+ verification_credit_miss_count += 1
264
+ if event_type == "skill_matches":
265
+ matches = payload.get("matches")
266
+ if isinstance(matches, list):
267
+ for item in matches:
268
+ if not isinstance(item, Mapping):
269
+ continue
270
+ name = str(item.get("name") or "").strip()
271
+ if name:
272
+ matched_skill_names.append(name)
273
+ if event_type == "tool_call":
274
+ tool_call_count += 1
275
+ tool_name = str(payload.get("name") or "").strip()
276
+ arguments_obj = payload.get("arguments")
277
+ arguments = arguments_obj if isinstance(arguments_obj, Mapping) else {}
278
+ if tool_name == "skill_read":
279
+ name = str(arguments.get("name") or "").strip()
280
+ if name:
281
+ skill_read_names.append(name)
282
+ continue
283
+ lifecycle_cli_commands.extend(
284
+ _extract_skill_lifecycle_cli_commands_from_tool_call(
285
+ tool_name=tool_name,
286
+ arguments=arguments,
287
+ )
288
+ )
289
+ manual_accessed, bundle_name = _manual_skill_bundle_access_from_tool_call(
290
+ tool_name=tool_name,
291
+ arguments=arguments,
292
+ workspace_root=workspace_root,
293
+ )
294
+ if manual_accessed:
295
+ manual_skill_bundle_access_count += 1
296
+ if bundle_name:
297
+ manual_skill_bundle_names.append(bundle_name)
298
+
299
+ matched_unique = _ordered_unique_strings(matched_skill_names)
300
+ skill_read_unique = _ordered_unique_strings(skill_read_names)
301
+ lifecycle_cli_unique = _ordered_unique_strings(lifecycle_cli_commands)
302
+ manual_unique = _ordered_unique_strings(manual_skill_bundle_names)
303
+ return {
304
+ "matched_skill_context_attached": bool(matched_unique),
305
+ "matched_skill_names": tuple(matched_unique),
306
+ "skill_read_called": bool(skill_read_names),
307
+ "skill_read_names": tuple(skill_read_unique),
308
+ "skill_read_call_count": len(skill_read_names),
309
+ "skill_lifecycle_cli_used": bool(lifecycle_cli_commands),
310
+ "skill_lifecycle_cli_commands": tuple(lifecycle_cli_unique),
311
+ "skill_lifecycle_cli_call_count": len(lifecycle_cli_commands),
312
+ "manual_skill_bundle_accessed": manual_skill_bundle_access_count > 0,
313
+ "manual_skill_bundle_names": tuple(manual_unique),
314
+ "manual_skill_bundle_access_count": manual_skill_bundle_access_count,
315
+ "tool_call_count": tool_call_count,
316
+ "completion_gate_failure_count": completion_gate_failure_count,
317
+ "completion_gate_incomplete_after_retries_count": (
318
+ completion_gate_incomplete_after_retries_count
319
+ ),
320
+ "forced_final_summary_count": forced_final_summary_count,
321
+ "verification_credit_miss_count": verification_credit_miss_count,
322
+ }
323
+
324
+
325
+ def _mode_metadata(mode_name: str) -> SkillsEvalMode | None:
326
+ return _SKILLS_EVAL_MODE_BY_NAME.get(str(mode_name or "").strip())
327
+
328
+
329
+ def _mode_skills_enabled(mode_name: str) -> bool:
330
+ mode = _mode_metadata(mode_name)
331
+ return bool(mode.skills_enabled) if mode is not None else False
332
+
333
+
334
+ def aggregate_skills_eval_records(
335
+ records: Sequence[SkillsEvalRecord],
336
+ ) -> dict[str, object]:
337
+ grouped: dict[str, list[SkillsEvalRecord]] = defaultdict(list)
338
+ for record in records:
339
+ grouped[record.mode].append(record)
340
+
341
+ executed_records = [record for record in records if record.status != "skipped"]
342
+ overall_expected = [record for record in executed_records if record.expected_skills]
343
+ overall_negative_controls = [
344
+ record for record in executed_records if not record.expected_skills
345
+ ]
346
+ overall_explicit_cases = [
347
+ record for record in executed_records if record.invocation_mode == "explicit_skill"
348
+ ]
349
+ overall_relevant_skill_usage_count = sum(
350
+ 1 for record in overall_expected if record.relevant_skill_used()
351
+ )
352
+ overall_false_negative_count = sum(
353
+ 1 for record in overall_expected if not record.relevant_skill_used()
354
+ )
355
+ overall_false_positive_count = sum(
356
+ 1 for record in overall_negative_controls if record.any_skill_activity()
357
+ )
358
+ overall_skill_read_count = sum(1 for record in executed_records if record.skill_read_called)
359
+ overall_lifecycle_cli_count = sum(
360
+ 1 for record in executed_records if record.skill_lifecycle_cli_used
361
+ )
362
+ overall_manual_skill_access_count = sum(
363
+ 1 for record in executed_records if record.manual_skill_bundle_accessed
364
+ )
365
+ overall_lifecycle_cli_call_count = sum(
366
+ int(record.skill_lifecycle_cli_call_count or 0) for record in executed_records
367
+ )
368
+ overall_explicit_success_count = sum(
369
+ 1 for record in overall_explicit_cases if record.passed is True
370
+ )
371
+ overall_passed_count = sum(1 for record in executed_records if record.passed is True)
372
+ skill_enabled_records = [
373
+ record for record in executed_records if _mode_skills_enabled(record.mode)
374
+ ]
375
+ skill_enabled_expected = [record for record in skill_enabled_records if record.expected_skills]
376
+ skill_enabled_relevant_skill_usage_count = sum(
377
+ 1 for record in skill_enabled_expected if record.relevant_skill_used()
378
+ )
379
+ skill_enabled_passed_count = sum(1 for record in skill_enabled_records if record.passed is True)
380
+
381
+ per_mode: dict[str, dict[str, object]] = {}
382
+ for mode_name, mode_records in grouped.items():
383
+ mode_cfg = _mode_metadata(mode_name)
384
+ executed = [record for record in mode_records if record.status != "skipped"]
385
+ expected = [record for record in executed if record.expected_skills]
386
+ negative_controls = [record for record in executed if not record.expected_skills]
387
+ explicit_cases = [
388
+ record for record in executed if record.invocation_mode == "explicit_skill"
389
+ ]
390
+ passed_count = sum(1 for record in executed if record.passed is True)
391
+ matched_trigger_count = sum(1 for record in expected if record.relevant_skill_used())
392
+ false_negative_count = sum(1 for record in expected if not record.relevant_skill_used())
393
+ false_positive_count = sum(1 for record in negative_controls if record.any_skill_activity())
394
+ skill_read_count = sum(1 for record in executed if record.skill_read_called)
395
+ lifecycle_cli_count = sum(1 for record in executed if record.skill_lifecycle_cli_used)
396
+ manual_skill_access_count = sum(
397
+ 1 for record in executed if record.manual_skill_bundle_accessed
398
+ )
399
+ lifecycle_cli_call_count = sum(
400
+ int(record.skill_lifecycle_cli_call_count or 0) for record in executed
401
+ )
402
+ explicit_success_count = sum(1 for record in explicit_cases if record.passed is True)
403
+ launch_runtime = _launch_runtime_summary(executed)
404
+ per_mode[mode_name] = {
405
+ "total_runs": len(mode_records),
406
+ "executed_runs": len(executed),
407
+ "skipped_runs": sum(1 for record in mode_records if record.status == "skipped"),
408
+ "passed_runs": passed_count,
409
+ "failed_runs": sum(1 for record in executed if record.passed is False),
410
+ "skills_enabled": bool(mode_cfg.skills_enabled) if mode_cfg is not None else None,
411
+ "conventions_enabled": (
412
+ bool(mode_cfg.conventions_enabled) if mode_cfg is not None else None
413
+ ),
414
+ "skills_auto_invoke": (
415
+ bool(mode_cfg.skills_auto_invoke) if mode_cfg is not None else None
416
+ ),
417
+ "pass_rate": _rate(passed_count, len(executed)),
418
+ "expected_skill_runs": len(expected),
419
+ "relevant_skill_usage_count": matched_trigger_count,
420
+ "skill_trigger_rate": _rate(matched_trigger_count, len(expected)),
421
+ "skill_read_rate": _rate(skill_read_count, len(executed)),
422
+ "skill_lifecycle_cli_rate": _rate(lifecycle_cli_count, len(executed)),
423
+ "skill_lifecycle_cli_call_count": lifecycle_cli_call_count,
424
+ "manual_skill_access_rate": _rate(manual_skill_access_count, len(executed)),
425
+ "false_negative_rate": _rate(false_negative_count, len(expected)),
426
+ "false_positive_rate": _rate(false_positive_count, len(negative_controls)),
427
+ "explicit_skill_runs": len(explicit_cases),
428
+ "explicit_skill_success_count": explicit_success_count,
429
+ "explicit_invocation_success_rate": _rate(
430
+ explicit_success_count,
431
+ len(explicit_cases),
432
+ ),
433
+ **launch_runtime,
434
+ }
435
+
436
+ per_skill: dict[str, dict[str, object]] = {}
437
+ expected_skill_records = [record for record in records if record.expected_skills]
438
+ skill_names = sorted(
439
+ {
440
+ expected_skill
441
+ for record in expected_skill_records
442
+ for expected_skill in record.expected_skills
443
+ }
444
+ )
445
+ for skill_name in skill_names:
446
+ relevant_records = [
447
+ record
448
+ for record in expected_skill_records
449
+ if any(
450
+ expected.casefold() == skill_name.casefold() for expected in record.expected_skills
451
+ )
452
+ and record.status != "skipped"
453
+ ]
454
+ per_skill[skill_name] = {
455
+ "runs": len(relevant_records),
456
+ "triggered": sum(
457
+ 1
458
+ for record in relevant_records
459
+ if any(
460
+ name.casefold() == skill_name.casefold()
461
+ for name in record.observed_skill_names()
462
+ )
463
+ ),
464
+ "passed": sum(1 for record in relevant_records if record.passed is True),
465
+ }
466
+
467
+ return {
468
+ "generated_at": datetime.now(UTC).isoformat(),
469
+ "result_count": len(records),
470
+ "executed_runs": len(executed_records),
471
+ "skipped_runs": sum(1 for record in records if record.status == "skipped"),
472
+ "passed_runs": overall_passed_count,
473
+ "failed_runs": sum(1 for record in executed_records if record.passed is False),
474
+ "pass_rate": _rate(overall_passed_count, len(executed_records)),
475
+ "pass_rate_all_modes": _rate(overall_passed_count, len(executed_records)),
476
+ "skill_enabled_executed_runs": len(skill_enabled_records),
477
+ "pass_rate_skill_enabled": _rate(skill_enabled_passed_count, len(skill_enabled_records)),
478
+ "expected_skill_runs": len(overall_expected),
479
+ "skill_eligible_runs": len(skill_enabled_expected),
480
+ "relevant_skill_usage_count": overall_relevant_skill_usage_count,
481
+ "relevant_skill_usage_count_all_modes": overall_relevant_skill_usage_count,
482
+ "relevant_skill_usage_rate": _rate(
483
+ overall_relevant_skill_usage_count,
484
+ len(overall_expected),
485
+ ),
486
+ "relevant_skill_usage_rate_all_modes": _rate(
487
+ overall_relevant_skill_usage_count,
488
+ len(overall_expected),
489
+ ),
490
+ "relevant_skill_usage_count_skill_enabled": skill_enabled_relevant_skill_usage_count,
491
+ "relevant_skill_usage_rate_skill_enabled": _rate(
492
+ skill_enabled_relevant_skill_usage_count,
493
+ len(skill_enabled_expected),
494
+ ),
495
+ "skill_read_rate": _rate(overall_skill_read_count, len(executed_records)),
496
+ "skill_lifecycle_cli_rate": _rate(overall_lifecycle_cli_count, len(executed_records)),
497
+ "skill_lifecycle_cli_call_count": overall_lifecycle_cli_call_count,
498
+ "manual_skill_access_rate": _rate(
499
+ overall_manual_skill_access_count,
500
+ len(executed_records),
501
+ ),
502
+ "false_negative_rate": _rate(overall_false_negative_count, len(overall_expected)),
503
+ "false_positive_rate": _rate(
504
+ overall_false_positive_count,
505
+ len(overall_negative_controls),
506
+ ),
507
+ "explicit_skill_runs": len(overall_explicit_cases),
508
+ "explicit_skill_success_count": overall_explicit_success_count,
509
+ "explicit_invocation_success_rate": _rate(
510
+ overall_explicit_success_count,
511
+ len(overall_explicit_cases),
512
+ ),
513
+ **_launch_runtime_summary(executed_records),
514
+ "modes": per_mode,
515
+ "per_skill": per_skill,
516
+ }
517
+
518
+
519
+ def summarize_skills_launch_candidate_metrics(
520
+ summary: Mapping[str, Any],
521
+ *,
522
+ config_snapshot: Mapping[str, Any] | None = None,
523
+ ) -> dict[str, object]:
524
+ modes_obj = summary.get("modes")
525
+ modes = modes_obj if isinstance(modes_obj, Mapping) else {}
526
+ default_skills_auto_invoke = None
527
+ if isinstance(config_snapshot, Mapping):
528
+ default_skills_auto_invoke = config_snapshot.get("skills_auto_invoke")
529
+
530
+ preferred_launch_modes = (
531
+ _AUTO_LAUNCH_MODE_NAMES if default_skills_auto_invoke is True else _MANUAL_LAUNCH_MODE_NAMES
532
+ )
533
+ fallback_launch_modes = (
534
+ _MANUAL_LAUNCH_MODE_NAMES if default_skills_auto_invoke is True else _AUTO_LAUNCH_MODE_NAMES
535
+ )
536
+ launch_mode_names = tuple(name for name in preferred_launch_modes if name in modes)
537
+ if not launch_mode_names:
538
+ launch_mode_names = tuple(name for name in fallback_launch_modes if name in modes)
539
+ if not launch_mode_names:
540
+ launch_mode_names = tuple(
541
+ sorted(
542
+ name
543
+ for name, payload_obj in modes.items()
544
+ if isinstance(payload_obj, Mapping) and bool(payload_obj.get("skills_enabled"))
545
+ )
546
+ )
547
+
548
+ skill_enabled_mode_names = tuple(
549
+ sorted(
550
+ name
551
+ for name, payload_obj in modes.items()
552
+ if isinstance(payload_obj, Mapping) and bool(payload_obj.get("skills_enabled"))
553
+ )
554
+ )
555
+
556
+ def _sum_mode_counts(mode_names: Sequence[str], key: str) -> int:
557
+ total = 0
558
+ for mode_name in mode_names:
559
+ payload_obj = modes.get(mode_name)
560
+ payload = payload_obj if isinstance(payload_obj, Mapping) else {}
561
+ total += int(payload.get(key) or 0)
562
+ return total
563
+
564
+ launch_executed_runs = _sum_mode_counts(launch_mode_names, "executed_runs")
565
+ launch_passed_runs = _sum_mode_counts(launch_mode_names, "passed_runs")
566
+ launch_skill_eligible_runs = _sum_mode_counts(launch_mode_names, "expected_skill_runs")
567
+ launch_relevant_skill_usage_count = _sum_mode_counts(
568
+ launch_mode_names, "relevant_skill_usage_count"
569
+ )
570
+ launch_explicit_skill_runs = _sum_mode_counts(launch_mode_names, "explicit_skill_runs")
571
+ launch_explicit_skill_success_count = _sum_mode_counts(
572
+ launch_mode_names, "explicit_skill_success_count"
573
+ )
574
+
575
+ return {
576
+ "launch_mode_basis": (
577
+ "skills_auto_invoke=true"
578
+ if default_skills_auto_invoke is True
579
+ else "skills_auto_invoke=false"
580
+ ),
581
+ "launch_mode_names": list(launch_mode_names),
582
+ "skill_enabled_mode_names": list(skill_enabled_mode_names),
583
+ "launch_mode_executed_runs": launch_executed_runs,
584
+ "launch_mode_passed_runs": launch_passed_runs,
585
+ "pass_rate_launch_modes": _rate(launch_passed_runs, launch_executed_runs),
586
+ "launch_skill_eligible_runs": launch_skill_eligible_runs,
587
+ "relevant_skill_usage_count_launch_modes": launch_relevant_skill_usage_count,
588
+ "relevant_skill_usage_rate_launch_modes": _rate(
589
+ launch_relevant_skill_usage_count,
590
+ launch_skill_eligible_runs,
591
+ ),
592
+ "explicit_skill_runs_launch_modes": launch_explicit_skill_runs,
593
+ "explicit_skill_success_count_launch_modes": launch_explicit_skill_success_count,
594
+ "explicit_invocation_success_rate_launch_modes": _rate(
595
+ launch_explicit_skill_success_count,
596
+ launch_explicit_skill_runs,
597
+ ),
598
+ }
599
+
600
+
601
+ def evaluate_skills_launch_readiness(
602
+ *,
603
+ summary: Mapping[str, Any],
604
+ config_snapshot: Mapping[str, Any] | None = None,
605
+ thresholds: Mapping[str, float] | None = None,
606
+ ) -> dict[str, object]:
607
+ effective_thresholds = dict(DEFAULT_SKILLS_LAUNCH_GATES)
608
+ if thresholds is not None:
609
+ for key, value in thresholds.items():
610
+ try:
611
+ effective_thresholds[str(key)] = float(value)
612
+ except (TypeError, ValueError):
613
+ continue
614
+
615
+ launch_metrics = summarize_skills_launch_candidate_metrics(
616
+ summary,
617
+ config_snapshot=config_snapshot,
618
+ )
619
+ launch_mode_names = tuple(launch_metrics.get("launch_mode_names") or ())
620
+ launch_label = ", ".join(launch_mode_names) if launch_mode_names else "launch-candidate modes"
621
+ pass_rate_actual = launch_metrics.get("pass_rate_launch_modes")
622
+ if pass_rate_actual is None:
623
+ pass_rate_actual = summary.get("pass_rate")
624
+ relevant_skill_usage_actual = launch_metrics.get("relevant_skill_usage_rate_launch_modes")
625
+ if relevant_skill_usage_actual is None:
626
+ relevant_skill_usage_actual = summary.get("relevant_skill_usage_rate_skill_enabled")
627
+ if relevant_skill_usage_actual is None:
628
+ relevant_skill_usage_actual = summary.get("relevant_skill_usage_rate")
629
+ explicit_skill_success_actual = launch_metrics.get(
630
+ "explicit_invocation_success_rate_launch_modes"
631
+ )
632
+ if explicit_skill_success_actual is None:
633
+ explicit_skill_success_actual = summary.get("explicit_invocation_success_rate")
634
+
635
+ gates = {
636
+ "pass_rate": _min_rate_gate(
637
+ actual=pass_rate_actual,
638
+ threshold=effective_thresholds["pass_rate_min"],
639
+ description=(
640
+ f"Pass rate across the current launch-candidate modes ({launch_label}) must stay strong on the launch suite."
641
+ ),
642
+ ),
643
+ "completion_gate_failure_rate": _max_rate_gate(
644
+ actual=summary.get("completion_gate_failure_rate"),
645
+ threshold=effective_thresholds["completion_gate_failure_rate_max"],
646
+ description="Completion-gate failure rate must stay below the public-launch bar.",
647
+ ),
648
+ "completion_gate_incomplete_after_retries_rate": _max_rate_gate(
649
+ actual=summary.get("completion_gate_incomplete_after_retries_rate"),
650
+ threshold=effective_thresholds["completion_gate_incomplete_after_retries_rate_max"],
651
+ description=(
652
+ "Completion-gate incomplete-after-retries should be essentially absent in a "
653
+ "production candidate."
654
+ ),
655
+ ),
656
+ "forced_final_summary_rate": _max_rate_gate(
657
+ actual=summary.get("forced_final_summary_rate"),
658
+ threshold=effective_thresholds["forced_final_summary_rate_max"],
659
+ description="Forced-final-summary fallback must stay rare on the launch suite.",
660
+ ),
661
+ "verification_credit_miss_rate": _max_rate_gate(
662
+ actual=summary.get("verification_credit_miss_rate"),
663
+ threshold=effective_thresholds["verification_credit_miss_rate_max"],
664
+ description="Equivalent real verification commands must be credited consistently.",
665
+ ),
666
+ "relevant_skill_usage_rate": _min_rate_gate(
667
+ actual=relevant_skill_usage_actual,
668
+ threshold=effective_thresholds["relevant_skill_usage_rate_min"],
669
+ description=(
670
+ "Skill-expected launch-candidate runs should show relevant observed skill usage "
671
+ "without counting control modes where skills are disabled."
672
+ ),
673
+ ),
674
+ "explicit_skill_success_rate": _min_rate_gate(
675
+ actual=explicit_skill_success_actual,
676
+ threshold=effective_thresholds["explicit_skill_success_rate_min"],
677
+ description="Explicit /skill flows must complete successfully on the launch suite.",
678
+ ),
679
+ }
680
+
681
+ default_skills_auto_invoke = None
682
+ if isinstance(config_snapshot, Mapping):
683
+ default_skills_auto_invoke = config_snapshot.get("skills_auto_invoke")
684
+
685
+ failing_gates = [
686
+ gate_name
687
+ for gate_name, payload in gates.items()
688
+ if str(payload.get("status") or "") != "pass"
689
+ ]
690
+ passing_gates = sorted(
691
+ gate_name
692
+ for gate_name, payload in gates.items()
693
+ if str(payload.get("status") or "") == "pass"
694
+ )
695
+ failing_gates = sorted(failing_gates)
696
+ production_ready = not failing_gates
697
+ return {
698
+ "evaluated_at": datetime.now(UTC).isoformat(),
699
+ "production_ready": production_ready,
700
+ "launch_metrics": launch_metrics,
701
+ "passing_gates": passing_gates,
702
+ "failing_gates": failing_gates,
703
+ "gates": gates,
704
+ "thresholds": effective_thresholds,
705
+ "default_skills_auto_invoke": default_skills_auto_invoke,
706
+ }
707
+
708
+
709
+ def render_skills_eval_summary_markdown(
710
+ *,
711
+ summary: Mapping[str, Any],
712
+ manifest_path: Path | None = None,
713
+ ) -> str:
714
+ lines = ["# Skills Eval Summary", ""]
715
+ if manifest_path is not None:
716
+ lines.append(f"- Manifest: `{manifest_path.as_posix()}`")
717
+ generated_at = str(summary.get("generated_at") or "").strip()
718
+ if generated_at:
719
+ lines.append(f"- Generated at: `{generated_at}`")
720
+ lines.extend(
721
+ [
722
+ "",
723
+ "| mode | executed | skipped | pass rate | trigger rate | skill_read rate | lifecycle CLI rate | manual access rate | false-negative | false-positive | explicit success |",
724
+ "| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
725
+ ]
726
+ )
727
+ modes_obj = summary.get("modes")
728
+ modes = modes_obj if isinstance(modes_obj, Mapping) else {}
729
+ for mode_name in sorted(modes):
730
+ item_obj = modes.get(mode_name)
731
+ item = item_obj if isinstance(item_obj, Mapping) else {}
732
+ lines.append(
733
+ "| {mode} | {executed} | {skipped} | {pass_rate} | {trigger} | {skill_read} | {lifecycle_cli} | {manual} | {fn} | {fp} | {explicit} |".format(
734
+ mode=mode_name,
735
+ executed=item.get("executed_runs", 0),
736
+ skipped=item.get("skipped_runs", 0),
737
+ pass_rate=_format_rate(item.get("pass_rate")),
738
+ trigger=_format_rate(item.get("skill_trigger_rate")),
739
+ skill_read=_format_rate(item.get("skill_read_rate")),
740
+ lifecycle_cli=_format_rate(item.get("skill_lifecycle_cli_rate")),
741
+ manual=_format_rate(item.get("manual_skill_access_rate")),
742
+ fn=_format_rate(item.get("false_negative_rate")),
743
+ fp=_format_rate(item.get("false_positive_rate")),
744
+ explicit=_format_rate(item.get("explicit_invocation_success_rate")),
745
+ )
746
+ )
747
+ lines.extend(
748
+ [
749
+ "",
750
+ "## Launch Runtime Metrics",
751
+ "",
752
+ f"- completion-gate failure rate: {_format_rate(summary.get('completion_gate_failure_rate'))} ({summary.get('completion_gate_failure_run_count', 0)} run(s), {summary.get('completion_gate_failure_count', 0)} event(s))",
753
+ f"- completion-gate incomplete-after-retries rate: {_format_rate(summary.get('completion_gate_incomplete_after_retries_rate'))} ({summary.get('completion_gate_incomplete_after_retries_run_count', 0)} run(s), {summary.get('completion_gate_incomplete_after_retries_count', 0)} event(s))",
754
+ f"- forced-final-summary rate: {_format_rate(summary.get('forced_final_summary_rate'))} ({summary.get('forced_final_summary_run_count', 0)} run(s), {summary.get('forced_final_summary_count', 0)} event(s))",
755
+ f"- verification-credit miss rate: {_format_rate(summary.get('verification_credit_miss_rate'))} ({summary.get('verification_credit_miss_run_count', 0)} run(s), {summary.get('verification_credit_miss_count', 0)} event(s))",
756
+ f"- overall pass rate (all completed modes): {_format_rate(summary.get('pass_rate_all_modes', summary.get('pass_rate')))}",
757
+ f"- launch-candidate pass rate: {_format_rate(summary.get('pass_rate_launch_modes', summary.get('pass_rate')))}",
758
+ f"- relevant skill usage rate (all modes): {_format_rate(summary.get('relevant_skill_usage_rate_all_modes', summary.get('relevant_skill_usage_rate')))}",
759
+ f"- relevant skill usage rate (skill-enabled modes): {_format_rate(summary.get('relevant_skill_usage_rate_skill_enabled', summary.get('relevant_skill_usage_rate')))}",
760
+ f"- relevant skill usage rate (launch-candidate modes): {_format_rate(summary.get('relevant_skill_usage_rate_launch_modes', summary.get('relevant_skill_usage_rate')))}",
761
+ f"- explicit /skill success rate: {_format_rate(summary.get('explicit_invocation_success_rate'))}",
762
+ ]
763
+ )
764
+ launch_mode_names = summary.get("launch_mode_names")
765
+ if isinstance(launch_mode_names, Sequence) and not isinstance(launch_mode_names, str):
766
+ names = [str(item).strip() for item in launch_mode_names if str(item).strip()]
767
+ if names:
768
+ lines.append(f"- launch-candidate modes: {', '.join(names)}")
769
+ release_gates_obj = summary.get("release_gates")
770
+ release_gates = release_gates_obj if isinstance(release_gates_obj, Mapping) else {}
771
+ gates_obj = release_gates.get("gates")
772
+ gates = gates_obj if isinstance(gates_obj, Mapping) else {}
773
+ if gates:
774
+ lines.extend(["", "## Release Gates", ""])
775
+ for gate_name in sorted(gates):
776
+ gate_obj = gates.get(gate_name)
777
+ gate = gate_obj if isinstance(gate_obj, Mapping) else {}
778
+ actual = gate.get("actual")
779
+ actual_text = _format_rate(actual) if isinstance(actual, float) else str(actual)
780
+ threshold_text = _gate_threshold_text(gate)
781
+ lines.append(
782
+ f"- `{gate_name}`: {str(gate.get('status') or 'unknown')} (actual={actual_text}, required={threshold_text})"
783
+ )
784
+ per_skill_obj = summary.get("per_skill")
785
+ per_skill = per_skill_obj if isinstance(per_skill_obj, Mapping) else {}
786
+ if per_skill:
787
+ lines.extend(["", "## Per-skill", ""])
788
+ for skill_name in sorted(per_skill):
789
+ item_obj = per_skill.get(skill_name)
790
+ item = item_obj if isinstance(item_obj, Mapping) else {}
791
+ lines.append(
792
+ f"- `{skill_name}`: runs={item.get('runs', 0)}, triggered={item.get('triggered', 0)}, passed={item.get('passed', 0)}"
793
+ )
794
+ lines.append("")
795
+ return "\n".join(lines)
796
+
797
+
798
+ def write_skills_eval_artifacts(
799
+ *,
800
+ output_dir: Path,
801
+ records: Sequence[SkillsEvalRecord],
802
+ summary: Mapping[str, Any],
803
+ manifest_path: Path | None = None,
804
+ cases: Sequence[SkillsEvalCase] | None = None,
805
+ ) -> SkillsEvalArtifacts:
806
+ output_dir.mkdir(parents=True, exist_ok=True)
807
+ results_path = output_dir / "results.jsonl"
808
+ summary_json_path = output_dir / "summary.json"
809
+ summary_md_path = output_dir / "summary.md"
810
+ normalized_cases_path = output_dir / "cases.json"
811
+
812
+ with results_path.open("w", encoding="utf-8") as fh:
813
+ for record in records:
814
+ fh.write(json.dumps(record.to_payload(output_dir=output_dir), ensure_ascii=True) + "\n")
815
+
816
+ summary_payload = dict(summary)
817
+ if manifest_path is not None:
818
+ summary_payload["manifest_path"] = manifest_path.resolve().as_posix()
819
+ summary_json_path.write_text(
820
+ json.dumps(summary_payload, indent=2, sort_keys=True) + "\n",
821
+ encoding="utf-8",
822
+ )
823
+ summary_md_path.write_text(
824
+ render_skills_eval_summary_markdown(summary=summary_payload, manifest_path=manifest_path),
825
+ encoding="utf-8",
826
+ )
827
+ if cases is not None:
828
+ normalized_cases_path.write_text(
829
+ json.dumps(
830
+ [_case_to_payload(case) for case in cases],
831
+ indent=2,
832
+ sort_keys=True,
833
+ )
834
+ + "\n",
835
+ encoding="utf-8",
836
+ )
837
+
838
+ return SkillsEvalArtifacts(
839
+ output_dir=output_dir,
840
+ results_path=results_path,
841
+ summary_json_path=summary_json_path,
842
+ summary_md_path=summary_md_path,
843
+ records=tuple(records),
844
+ summary=dict(summary_payload),
845
+ )
846
+
847
+
848
+ def run_skills_eval_suite(
849
+ *,
850
+ cases: Sequence[SkillsEvalCase],
851
+ modes: Sequence[SkillsEvalMode],
852
+ output_dir: Path,
853
+ executor: SkillsEvalExecutor,
854
+ verification_runner: SkillsEvalVerificationRunner | None = None,
855
+ manifest_path: Path | None = None,
856
+ max_steps: int = 25,
857
+ temp_base_dir: Path | None = None,
858
+ ) -> SkillsEvalArtifacts:
859
+ results: list[SkillsEvalRecord] = []
860
+ output_dir.mkdir(parents=True, exist_ok=True)
861
+ sessions_dir = output_dir / "sessions"
862
+ verification_runner = verification_runner or run_shell_verification_command
863
+
864
+ for case_idx, case in enumerate(cases, start=1):
865
+ for mode in modes:
866
+ if case.invocation_mode == "explicit_skill" and not mode.skills_enabled:
867
+ results.append(
868
+ SkillsEvalRecord(
869
+ case_id=case.id,
870
+ mode=mode.name,
871
+ workspace=case.workspace,
872
+ task=case.task,
873
+ invocation_mode=case.invocation_mode,
874
+ explicit_skill_name=case.explicit_skill_name,
875
+ expected_skills=case.expected_skills,
876
+ tags=case.tags,
877
+ notes=case.notes,
878
+ status="skipped",
879
+ passed=None,
880
+ skip_reason="explicit skill invocation requires a skills-enabled mode",
881
+ agent_exit_code=None,
882
+ verification_command=case.verification_command,
883
+ verification_exit_code=None,
884
+ verification_output_preview=None,
885
+ skills_advertised_present=False,
886
+ repo_conventions_present=False,
887
+ matched_skill_context_attached=False,
888
+ matched_skill_names=(),
889
+ explicit_skill_context_used=False,
890
+ skill_read_called=False,
891
+ skill_read_names=(),
892
+ skill_read_call_count=0,
893
+ skill_lifecycle_cli_used=False,
894
+ skill_lifecycle_cli_commands=(),
895
+ skill_lifecycle_cli_call_count=0,
896
+ manual_skill_bundle_accessed=False,
897
+ manual_skill_bundle_names=(),
898
+ manual_skill_bundle_access_count=0,
899
+ tool_call_count=0,
900
+ session_log_path=None,
901
+ session_artifact_root=None,
902
+ )
903
+ )
904
+ continue
905
+
906
+ try:
907
+ with prepared_skills_eval_workspace(
908
+ source_workspace=case.workspace,
909
+ mode=mode,
910
+ temp_base_dir=temp_base_dir,
911
+ ) as run_workspace:
912
+ request = SkillsEvalExecutionRequest(
913
+ case=case,
914
+ mode=mode,
915
+ workspace=run_workspace,
916
+ output_dir=output_dir,
917
+ sessions_dir=sessions_dir,
918
+ session_id=_build_session_id(
919
+ case_id=case.id, mode_name=mode.name, ordinal=case_idx
920
+ ),
921
+ max_steps=max_steps,
922
+ )
923
+ execution = executor.execute(request)
924
+ verification = None
925
+ if case.verification_command:
926
+ verification = verification_runner(
927
+ workspace=run_workspace,
928
+ command=case.verification_command,
929
+ )
930
+ except Exception as exc: # noqa: BLE001
931
+ results.append(
932
+ SkillsEvalRecord(
933
+ case_id=case.id,
934
+ mode=mode.name,
935
+ workspace=case.workspace,
936
+ task=case.task,
937
+ invocation_mode=case.invocation_mode,
938
+ explicit_skill_name=case.explicit_skill_name,
939
+ expected_skills=case.expected_skills,
940
+ tags=case.tags,
941
+ notes=case.notes,
942
+ status="failed",
943
+ passed=False,
944
+ skip_reason=None,
945
+ agent_exit_code=None,
946
+ verification_command=case.verification_command,
947
+ verification_exit_code=None,
948
+ verification_output_preview=None,
949
+ skills_advertised_present=False,
950
+ repo_conventions_present=False,
951
+ matched_skill_context_attached=False,
952
+ matched_skill_names=(),
953
+ explicit_skill_context_used=False,
954
+ skill_read_called=False,
955
+ skill_read_names=(),
956
+ skill_read_call_count=0,
957
+ skill_lifecycle_cli_used=False,
958
+ skill_lifecycle_cli_commands=(),
959
+ skill_lifecycle_cli_call_count=0,
960
+ manual_skill_bundle_accessed=False,
961
+ manual_skill_bundle_names=(),
962
+ manual_skill_bundle_access_count=0,
963
+ tool_call_count=0,
964
+ session_log_path=None,
965
+ session_artifact_root=None,
966
+ error=str(exc),
967
+ )
968
+ )
969
+ continue
970
+
971
+ verification_exit_code = verification.exit_code if verification is not None else None
972
+ status = (
973
+ "passed"
974
+ if execution.agent_exit_code == 0
975
+ and (verification_exit_code is None or verification_exit_code == 0)
976
+ else "failed"
977
+ )
978
+ results.append(
979
+ SkillsEvalRecord(
980
+ case_id=case.id,
981
+ mode=mode.name,
982
+ workspace=case.workspace,
983
+ task=case.task,
984
+ invocation_mode=case.invocation_mode,
985
+ explicit_skill_name=case.explicit_skill_name,
986
+ expected_skills=case.expected_skills,
987
+ tags=case.tags,
988
+ notes=case.notes,
989
+ status=status,
990
+ passed=status == "passed",
991
+ skip_reason=None,
992
+ agent_exit_code=execution.agent_exit_code,
993
+ verification_command=case.verification_command,
994
+ verification_exit_code=verification_exit_code,
995
+ verification_output_preview=(
996
+ verification.output_preview if verification is not None else None
997
+ ),
998
+ skills_advertised_present=execution.skills_advertised_present,
999
+ repo_conventions_present=execution.repo_conventions_present,
1000
+ matched_skill_context_attached=execution.matched_skill_context_attached,
1001
+ matched_skill_names=execution.matched_skill_names,
1002
+ explicit_skill_context_used=execution.explicit_skill_context_used,
1003
+ skill_read_called=execution.skill_read_called,
1004
+ skill_read_names=execution.skill_read_names,
1005
+ skill_read_call_count=execution.skill_read_call_count,
1006
+ skill_lifecycle_cli_used=execution.skill_lifecycle_cli_used,
1007
+ skill_lifecycle_cli_commands=execution.skill_lifecycle_cli_commands,
1008
+ skill_lifecycle_cli_call_count=execution.skill_lifecycle_cli_call_count,
1009
+ manual_skill_bundle_accessed=execution.manual_skill_bundle_accessed,
1010
+ manual_skill_bundle_names=execution.manual_skill_bundle_names,
1011
+ manual_skill_bundle_access_count=execution.manual_skill_bundle_access_count,
1012
+ tool_call_count=execution.tool_call_count,
1013
+ completion_gate_failure_count=execution.completion_gate_failure_count,
1014
+ completion_gate_incomplete_after_retries_count=(
1015
+ execution.completion_gate_incomplete_after_retries_count
1016
+ ),
1017
+ forced_final_summary_count=execution.forced_final_summary_count,
1018
+ verification_credit_miss_count=execution.verification_credit_miss_count,
1019
+ session_log_path=execution.session_log_path,
1020
+ session_artifact_root=execution.session_artifact_root,
1021
+ error=execution.error,
1022
+ )
1023
+ )
1024
+
1025
+ summary = aggregate_skills_eval_records(results)
1026
+ return write_skills_eval_artifacts(
1027
+ output_dir=output_dir,
1028
+ records=results,
1029
+ summary=summary,
1030
+ manifest_path=manifest_path,
1031
+ cases=cases,
1032
+ )
1033
+
1034
+
1035
+ def run_shell_verification_command(
1036
+ *,
1037
+ workspace: Path,
1038
+ command: str,
1039
+ ) -> SkillsEvalVerificationResult:
1040
+ proc = subprocess.run(
1041
+ command,
1042
+ shell=True,
1043
+ cwd=workspace,
1044
+ capture_output=True,
1045
+ text=True,
1046
+ check=False,
1047
+ )
1048
+ output = (str(proc.stdout or "") + str(proc.stderr or "")).strip()
1049
+ preview = output[:500]
1050
+ return SkillsEvalVerificationResult(exit_code=proc.returncode, output_preview=preview)
1051
+
1052
+
1053
+ def default_skills_eval_output_dir(*, root: Path | None = None) -> Path:
1054
+ base_root = (root or Path.cwd()).resolve()
1055
+ timestamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
1056
+ return base_root / ".alysis" / "evals" / "skills" / timestamp
1057
+
1058
+
1059
+ def _build_session_id(*, case_id: str, mode_name: str, ordinal: int) -> str:
1060
+ safe_case = "".join(ch if ch.isalnum() or ch in {"-", "_"} else "-" for ch in case_id)
1061
+ safe_mode = "".join(ch if ch.isalnum() or ch in {"-", "_"} else "-" for ch in mode_name)
1062
+ return f"skills_eval_{ordinal:03d}_{safe_case}_{safe_mode}"
1063
+
1064
+
1065
+ def _mask_conventions_in_workspace(workspace: Path) -> None:
1066
+ for path in workspace.rglob("*"):
1067
+ if not path.is_file():
1068
+ continue
1069
+ if path.name not in _CONVENTION_FILENAMES:
1070
+ continue
1071
+ path.unlink()
1072
+
1073
+
1074
+ def _mask_project_skill_roots_in_workspace(workspace: Path) -> None:
1075
+ for root_parts in project_skill_root_relative_paths():
1076
+ target = workspace.joinpath(*root_parts)
1077
+ if not target.exists():
1078
+ continue
1079
+ if target.is_symlink() or target.is_file():
1080
+ target.unlink()
1081
+ continue
1082
+ shutil.rmtree(target)
1083
+
1084
+
1085
+ def _manual_skill_bundle_access_from_tool_call(
1086
+ *,
1087
+ tool_name: str,
1088
+ arguments: Mapping[str, Any],
1089
+ workspace_root: Path | None,
1090
+ ) -> tuple[bool, str | None]:
1091
+ path_argument_name = _manual_skill_tool_path_argument_name(tool_name)
1092
+ if path_argument_name is None:
1093
+ return False, None
1094
+ path_value = arguments.get(path_argument_name)
1095
+ if not isinstance(path_value, str):
1096
+ return False, None
1097
+ if tool_name in {"fs_list", "search_rg"} and path_argument_name == "root_path":
1098
+ if str(path_value).strip() in {"", "."}:
1099
+ return False, None
1100
+ return _classify_manual_skill_bundle_path(path_value, workspace_root=workspace_root)
1101
+
1102
+
1103
+ def _manual_skill_tool_path_argument_name(tool_name: str) -> str | None:
1104
+ if tool_name in {"fs_read", "fs_read_lines"}:
1105
+ return "path"
1106
+ if tool_name in {"fs_list", "search_rg"}:
1107
+ return "root_path"
1108
+ return None
1109
+
1110
+
1111
+ def _extract_skill_lifecycle_cli_commands_from_tool_call(
1112
+ *,
1113
+ tool_name: str,
1114
+ arguments: Mapping[str, Any],
1115
+ ) -> tuple[str, ...]:
1116
+ normalized_tool = str(tool_name or "").strip()
1117
+ if normalized_tool == "shell_run":
1118
+ command = str(arguments.get("cmd") or "").strip()
1119
+ return _extract_skill_lifecycle_cli_commands_from_text(command)
1120
+ if normalized_tool == "verify_run":
1121
+ commands_obj = arguments.get("commands")
1122
+ if not isinstance(commands_obj, list):
1123
+ return ()
1124
+ commands: list[str] = []
1125
+ for item in commands_obj:
1126
+ value = str(item or "").strip()
1127
+ if not value:
1128
+ continue
1129
+ commands.extend(_extract_skill_lifecycle_cli_commands_from_text(value))
1130
+ return tuple(commands)
1131
+ return ()
1132
+
1133
+
1134
+ def _extract_skill_lifecycle_cli_commands_from_text(command: str) -> tuple[str, ...]:
1135
+ normalized = " ".join(str(command or "").casefold().split())
1136
+ if not normalized:
1137
+ return ()
1138
+ matches = [
1139
+ match.group(1).strip() for match in _SKILL_LIFECYCLE_COMMAND_PATTERN.finditer(normalized)
1140
+ ]
1141
+ return tuple(matches)
1142
+
1143
+
1144
+ def _classify_manual_skill_bundle_path(
1145
+ raw_path: str,
1146
+ *,
1147
+ workspace_root: Path | None,
1148
+ ) -> tuple[bool, str | None]:
1149
+ for candidate_parts in _candidate_relative_path_parts(raw_path, workspace_root=workspace_root):
1150
+ for root_parts in project_skill_root_relative_paths():
1151
+ if len(candidate_parts) < len(root_parts):
1152
+ continue
1153
+ if tuple(candidate_parts[: len(root_parts)]) != tuple(root_parts):
1154
+ continue
1155
+ if len(candidate_parts) == len(root_parts):
1156
+ return True, None
1157
+ return True, candidate_parts[len(root_parts)]
1158
+ return False, None
1159
+
1160
+
1161
+ def _candidate_relative_path_parts(
1162
+ raw_path: str,
1163
+ *,
1164
+ workspace_root: Path | None,
1165
+ ) -> tuple[tuple[str, ...], ...]:
1166
+ candidates: list[tuple[str, ...]] = []
1167
+ text = str(raw_path or "").strip()
1168
+ if not text:
1169
+ return ()
1170
+
1171
+ if workspace_root is not None:
1172
+ try:
1173
+ relative = Path(text).expanduser().resolve().relative_to(workspace_root.resolve())
1174
+ except (OSError, RuntimeError, ValueError):
1175
+ relative = None
1176
+ if relative is not None:
1177
+ candidates.append(relative.parts)
1178
+
1179
+ normalized = text.replace("\\", "/")
1180
+ parts = tuple(part for part in normalized.split("/") if part and part != ".")
1181
+ if parts and not (len(parts) > 1 and parts[0].endswith(":")):
1182
+ candidates.append(parts)
1183
+
1184
+ ordered: list[tuple[str, ...]] = []
1185
+ seen: set[tuple[str, ...]] = set()
1186
+ for candidate in candidates:
1187
+ if candidate in seen:
1188
+ continue
1189
+ seen.add(candidate)
1190
+ ordered.append(candidate)
1191
+ return tuple(ordered)
1192
+
1193
+
1194
+ def _normalized_optional_string(value: object) -> str | None:
1195
+ text = str(value or "").strip()
1196
+ return text or None
1197
+
1198
+
1199
+ def _skills_eval_tempdir_prefix(mode_name: str) -> str:
1200
+ slug = re.sub(r"[^a-z0-9]+", "-", str(mode_name or "").strip().lower()).strip("-")
1201
+ if not slug:
1202
+ slug = "mode"
1203
+ return f"se-{slug[:12]}-"
1204
+
1205
+
1206
+ def _normalized_string_tuple(value: object) -> tuple[str, ...]:
1207
+ if value is None:
1208
+ return ()
1209
+ if not isinstance(value, list):
1210
+ raise ValueError("expected a JSON array of strings")
1211
+ return tuple(_ordered_unique_strings(str(item or "").strip() for item in value))
1212
+
1213
+
1214
+ def _ordered_unique_strings(values: Iterable[str]) -> list[str]:
1215
+ out: list[str] = []
1216
+ seen: set[str] = set()
1217
+ for raw in values:
1218
+ value = str(raw or "").strip()
1219
+ if not value:
1220
+ continue
1221
+ lowered = value.casefold()
1222
+ if lowered in seen:
1223
+ continue
1224
+ seen.add(lowered)
1225
+ out.append(value)
1226
+ return out
1227
+
1228
+
1229
+ def _case_to_payload(case: SkillsEvalCase) -> dict[str, object]:
1230
+ return {
1231
+ "id": case.id,
1232
+ "workspace": case.workspace.as_posix(),
1233
+ "task": case.task,
1234
+ "invocation_mode": case.invocation_mode,
1235
+ "explicit_skill_name": case.explicit_skill_name,
1236
+ "expected_skills": list(case.expected_skills),
1237
+ "verification_command": case.verification_command,
1238
+ "tags": list(case.tags),
1239
+ "notes": case.notes,
1240
+ }
1241
+
1242
+
1243
+ def _rate(numerator: int, denominator: int) -> float | None:
1244
+ if denominator <= 0:
1245
+ return None
1246
+ return round(numerator / denominator, 4)
1247
+
1248
+
1249
+ def _format_rate(value: object) -> str:
1250
+ if value is None:
1251
+ return "-"
1252
+ if isinstance(value, float):
1253
+ return f"{value * 100:.1f}%"
1254
+ try:
1255
+ numeric = float(value)
1256
+ except (TypeError, ValueError):
1257
+ return "-"
1258
+ return f"{numeric * 100:.1f}%"
1259
+
1260
+
1261
+ def _launch_runtime_summary(records: Sequence[SkillsEvalRecord]) -> dict[str, object]:
1262
+ executed = [record for record in records if record.status != "skipped"]
1263
+ return _counter_rate_summary(
1264
+ executed,
1265
+ metric_fields=(
1266
+ "completion_gate_failure_count",
1267
+ "completion_gate_incomplete_after_retries_count",
1268
+ "forced_final_summary_count",
1269
+ "verification_credit_miss_count",
1270
+ ),
1271
+ )
1272
+
1273
+
1274
+ def _counter_rate_summary(
1275
+ records: Sequence[SkillsEvalRecord],
1276
+ *,
1277
+ metric_fields: Sequence[str],
1278
+ ) -> dict[str, object]:
1279
+ summary: dict[str, object] = {}
1280
+ executed_count = len(records)
1281
+ for field_name in metric_fields:
1282
+ total_count = sum(int(getattr(record, field_name, 0) or 0) for record in records)
1283
+ run_count = sum(1 for record in records if int(getattr(record, field_name, 0) or 0) > 0)
1284
+ metric_prefix = field_name.removesuffix("_count")
1285
+ summary[field_name] = total_count
1286
+ summary[f"{metric_prefix}_run_count"] = run_count
1287
+ summary[f"{metric_prefix}_rate"] = _rate(run_count, executed_count)
1288
+ return summary
1289
+
1290
+
1291
+ def _event_is_verification_credit_miss(*, event_type: str, payload: Mapping[str, Any]) -> bool:
1292
+ if event_type not in (_COMPLETION_GATE_FAILURE_EVENTS | _COMPLETION_GATE_INCOMPLETE_EVENTS):
1293
+ return False
1294
+ stage = str(payload.get("stage") or "").strip()
1295
+ if stage not in _VERIFICATION_CREDIT_MISS_STAGES:
1296
+ return False
1297
+ state_obj = payload.get("state")
1298
+ state = state_obj if isinstance(state_obj, Mapping) else {}
1299
+ try:
1300
+ verification_attempt_count = int(state.get("verification_attempt_count") or 0)
1301
+ except (TypeError, ValueError):
1302
+ verification_attempt_count = 0
1303
+ return verification_attempt_count > 0
1304
+
1305
+
1306
+ def _max_rate_gate(*, actual: object, threshold: float, description: str) -> dict[str, object]:
1307
+ actual_rate = _coerce_rate(actual)
1308
+ status = "fail" if actual_rate is None or actual_rate > threshold else "pass"
1309
+ return {
1310
+ "status": status,
1311
+ "actual": actual_rate,
1312
+ "max_allowed": threshold,
1313
+ "description": description,
1314
+ }
1315
+
1316
+
1317
+ def _min_rate_gate(*, actual: object, threshold: float, description: str) -> dict[str, object]:
1318
+ actual_rate = _coerce_rate(actual)
1319
+ status = "fail" if actual_rate is None or actual_rate < threshold else "pass"
1320
+ return {
1321
+ "status": status,
1322
+ "actual": actual_rate,
1323
+ "min_required": threshold,
1324
+ "description": description,
1325
+ }
1326
+
1327
+
1328
+ def _coerce_rate(value: object) -> float | None:
1329
+ if value is None:
1330
+ return None
1331
+ try:
1332
+ return float(value)
1333
+ except (TypeError, ValueError):
1334
+ return None
1335
+
1336
+
1337
+ def _gate_threshold_text(gate: Mapping[str, Any]) -> str:
1338
+ if "max_allowed" in gate:
1339
+ return f"<= {_format_rate(gate.get('max_allowed'))}"
1340
+ if "min_required" in gate:
1341
+ return f">= {_format_rate(gate.get('min_required'))}"
1342
+ if "required" in gate:
1343
+ return repr(gate.get("required"))
1344
+ return "-"