openlanes 3.0.0

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 (466) hide show
  1. checksums.yaml +7 -0
  2. data/agents/hansolo-reviewer.md +59 -0
  3. data/agents/lando-broker.md +70 -0
  4. data/agents/leah-researcher.md +60 -0
  5. data/agents/luke-backend.md +73 -0
  6. data/agents/palpatine-planner.md +61 -0
  7. data/agents/r2d2-mechanic.md +57 -0
  8. data/agents/rey-frontend.md +68 -0
  9. data/agents/yoda-writer.md +67 -0
  10. data/bin/factory-e2e +157 -0
  11. data/bin/plan-number +164 -0
  12. data/bin/setup +26 -0
  13. data/evals/cases/hansolo-reviewer/review-medium/fail/plan/plan.md +3 -0
  14. data/evals/cases/hansolo-reviewer/review-medium/fail/plan/pull-requests.md +12 -0
  15. data/evals/cases/hansolo-reviewer/review-medium/fail/plan/spec.md +5 -0
  16. data/evals/cases/hansolo-reviewer/review-medium/fail/repo/lib/cart.rb +9 -0
  17. data/evals/cases/hansolo-reviewer/review-medium/fail/repo/spec/cart_spec.rb +7 -0
  18. data/evals/cases/hansolo-reviewer/review-medium/fail/run.json +1 -0
  19. data/evals/cases/hansolo-reviewer/review-medium/pass/plan/plan.md +3 -0
  20. data/evals/cases/hansolo-reviewer/review-medium/pass/plan/pull-requests.md +15 -0
  21. data/evals/cases/hansolo-reviewer/review-medium/pass/plan/spec.md +5 -0
  22. data/evals/cases/hansolo-reviewer/review-medium/pass/repo/lib/cart.rb +9 -0
  23. data/evals/cases/hansolo-reviewer/review-medium/pass/repo/spec/cart_spec.rb +7 -0
  24. data/evals/cases/hansolo-reviewer/review-medium/pass/run.json +1 -0
  25. data/evals/cases/hansolo-reviewer/review-medium.yml +60 -0
  26. data/evals/cases/lando-broker/unblock-fast/fail/plan/blocked.md +9 -0
  27. data/evals/cases/lando-broker/unblock-fast/fail/plan/spec.md +16 -0
  28. data/evals/cases/lando-broker/unblock-fast/fail/run.json +1 -0
  29. data/evals/cases/lando-broker/unblock-fast/pass/plan/spec.md +18 -0
  30. data/evals/cases/lando-broker/unblock-fast/pass/run.json +1 -0
  31. data/evals/cases/lando-broker/unblock-fast.yml +44 -0
  32. data/evals/cases/leah-researcher/research-deep/fail/plan/spec.md +26 -0
  33. data/evals/cases/leah-researcher/research-deep/fail/run.json +1 -0
  34. data/evals/cases/leah-researcher/research-deep/pass/plan/spec.md +39 -0
  35. data/evals/cases/leah-researcher/research-deep/pass/run.json +1 -0
  36. data/evals/cases/leah-researcher/research-deep.yml +51 -0
  37. data/evals/cases/leah-researcher/research-fast/fail/plan/spec.md +27 -0
  38. data/evals/cases/leah-researcher/research-fast/fail/run.json +1 -0
  39. data/evals/cases/leah-researcher/research-fast/pass/plan/spec.md +23 -0
  40. data/evals/cases/leah-researcher/research-fast/pass/run.json +1 -0
  41. data/evals/cases/leah-researcher/research-fast.yml +55 -0
  42. data/evals/cases/luke-backend/build-medium/fail/plan/plan-backend.md +7 -0
  43. data/evals/cases/luke-backend/build-medium/fail/plan/plan-frontend.md +1 -0
  44. data/evals/cases/luke-backend/build-medium/fail/plan/plan.md +3 -0
  45. data/evals/cases/luke-backend/build-medium/fail/plan/pull-requests.md +8 -0
  46. data/evals/cases/luke-backend/build-medium/fail/plan/spec.md +8 -0
  47. data/evals/cases/luke-backend/build-medium/fail/repo/Gemfile +2 -0
  48. data/evals/cases/luke-backend/build-medium/fail/repo/lib/cart.rb +11 -0
  49. data/evals/cases/luke-backend/build-medium/fail/repo/spec/cart_spec.rb +7 -0
  50. data/evals/cases/luke-backend/build-medium/fail/run.json +1 -0
  51. data/evals/cases/luke-backend/build-medium/pass/plan/plan-backend.md +7 -0
  52. data/evals/cases/luke-backend/build-medium/pass/plan/plan-frontend.md +1 -0
  53. data/evals/cases/luke-backend/build-medium/pass/plan/plan.md +3 -0
  54. data/evals/cases/luke-backend/build-medium/pass/plan/pull-requests.md +8 -0
  55. data/evals/cases/luke-backend/build-medium/pass/plan/spec.md +8 -0
  56. data/evals/cases/luke-backend/build-medium/pass/repo/Gemfile +2 -0
  57. data/evals/cases/luke-backend/build-medium/pass/repo/lib/cart.rb +11 -0
  58. data/evals/cases/luke-backend/build-medium/pass/repo/spec/cart_spec.rb +11 -0
  59. data/evals/cases/luke-backend/build-medium/pass/run.json +1 -0
  60. data/evals/cases/luke-backend/build-medium.yml +68 -0
  61. data/evals/cases/palpatine-planner/plan-deep/fail/plan/plan-backend.md +6 -0
  62. data/evals/cases/palpatine-planner/plan-deep/fail/plan/plan.md +8 -0
  63. data/evals/cases/palpatine-planner/plan-deep/fail/plan/spec.md +14 -0
  64. data/evals/cases/palpatine-planner/plan-deep/fail/run.json +1 -0
  65. data/evals/cases/palpatine-planner/plan-deep/pass/plan/plan-backend.md +32 -0
  66. data/evals/cases/palpatine-planner/plan-deep/pass/plan/plan-frontend.md +1 -0
  67. data/evals/cases/palpatine-planner/plan-deep/pass/plan/plan.md +28 -0
  68. data/evals/cases/palpatine-planner/plan-deep/pass/plan/spec.md +14 -0
  69. data/evals/cases/palpatine-planner/plan-deep/pass/run.json +1 -0
  70. data/evals/cases/palpatine-planner/plan-deep.yml +50 -0
  71. data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan-backend.md +48 -0
  72. data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan-frontend.md +1 -0
  73. data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan.md +12 -0
  74. data/evals/cases/palpatine-planner/plan-fast/fail/plan/spec.md +12 -0
  75. data/evals/cases/palpatine-planner/plan-fast/fail/run.json +1 -0
  76. data/evals/cases/palpatine-planner/plan-fast/pass/plan/plan-backend.md +8 -0
  77. data/evals/cases/palpatine-planner/plan-fast/pass/plan/plan-frontend.md +1 -0
  78. data/evals/cases/palpatine-planner/plan-fast/pass/plan/plan.md +12 -0
  79. data/evals/cases/palpatine-planner/plan-fast/pass/plan/spec.md +12 -0
  80. data/evals/cases/palpatine-planner/plan-fast/pass/run.json +1 -0
  81. data/evals/cases/palpatine-planner/plan-fast.yml +47 -0
  82. data/evals/cases/r2d2-mechanic/task-fast/fail/plan/plan.md +7 -0
  83. data/evals/cases/r2d2-mechanic/task-fast/fail/plan/pull-requests.md +6 -0
  84. data/evals/cases/r2d2-mechanic/task-fast/fail/plan/spec.md +6 -0
  85. data/evals/cases/r2d2-mechanic/task-fast/fail/repo/README.md +3 -0
  86. data/evals/cases/r2d2-mechanic/task-fast/fail/repo/lib/greeter/formatter.rb +12 -0
  87. data/evals/cases/r2d2-mechanic/task-fast/fail/repo/lib/greeter.rb +15 -0
  88. data/evals/cases/r2d2-mechanic/task-fast/fail/repo/spec/greeter_spec.rb +7 -0
  89. data/evals/cases/r2d2-mechanic/task-fast/fail/run.json +1 -0
  90. data/evals/cases/r2d2-mechanic/task-fast/pass/plan/plan.md +7 -0
  91. data/evals/cases/r2d2-mechanic/task-fast/pass/plan/pull-requests.md +6 -0
  92. data/evals/cases/r2d2-mechanic/task-fast/pass/plan/spec.md +6 -0
  93. data/evals/cases/r2d2-mechanic/task-fast/pass/repo/README.md +3 -0
  94. data/evals/cases/r2d2-mechanic/task-fast/pass/repo/lib/greeter.rb +3 -0
  95. data/evals/cases/r2d2-mechanic/task-fast/pass/repo/spec/greeter_spec.rb +7 -0
  96. data/evals/cases/r2d2-mechanic/task-fast/pass/run.json +1 -0
  97. data/evals/cases/r2d2-mechanic/task-fast.yml +59 -0
  98. data/evals/cases/r2d2-mechanic/task-medium/fail/plan/plan.md +7 -0
  99. data/evals/cases/r2d2-mechanic/task-medium/fail/plan/pull-requests.md +6 -0
  100. data/evals/cases/r2d2-mechanic/task-medium/fail/plan/spec.md +6 -0
  101. data/evals/cases/r2d2-mechanic/task-medium/fail/repo/Gemfile +2 -0
  102. data/evals/cases/r2d2-mechanic/task-medium/fail/repo/lib/greeter.rb +6 -0
  103. data/evals/cases/r2d2-mechanic/task-medium/fail/repo/spec/greeter_spec.rb +11 -0
  104. data/evals/cases/r2d2-mechanic/task-medium/fail/run.json +1 -0
  105. data/evals/cases/r2d2-mechanic/task-medium/pass/plan/plan.md +7 -0
  106. data/evals/cases/r2d2-mechanic/task-medium/pass/plan/pull-requests.md +6 -0
  107. data/evals/cases/r2d2-mechanic/task-medium/pass/plan/spec.md +6 -0
  108. data/evals/cases/r2d2-mechanic/task-medium/pass/repo/Gemfile +2 -0
  109. data/evals/cases/r2d2-mechanic/task-medium/pass/repo/lib/greeter.rb +6 -0
  110. data/evals/cases/r2d2-mechanic/task-medium/pass/repo/spec/greeter_spec.rb +11 -0
  111. data/evals/cases/r2d2-mechanic/task-medium/pass/run.json +1 -0
  112. data/evals/cases/r2d2-mechanic/task-medium.yml +58 -0
  113. data/evals/cases/rey-frontend/ui-medium/fail/plan/plan-frontend.md +7 -0
  114. data/evals/cases/rey-frontend/ui-medium/fail/plan/plan.md +4 -0
  115. data/evals/cases/rey-frontend/ui-medium/fail/plan/pull-requests.md +8 -0
  116. data/evals/cases/rey-frontend/ui-medium/fail/plan/spec.md +5 -0
  117. data/evals/cases/rey-frontend/ui-medium/fail/repo/lib/cart.rb +5 -0
  118. data/evals/cases/rey-frontend/ui-medium/fail/repo/web/cart_badge.js +4 -0
  119. data/evals/cases/rey-frontend/ui-medium/fail/repo/web/cart_badge.test.js +3 -0
  120. data/evals/cases/rey-frontend/ui-medium/fail/run.json +1 -0
  121. data/evals/cases/rey-frontend/ui-medium/pass/plan/plan-frontend.md +7 -0
  122. data/evals/cases/rey-frontend/ui-medium/pass/plan/plan.md +4 -0
  123. data/evals/cases/rey-frontend/ui-medium/pass/plan/pull-requests.md +8 -0
  124. data/evals/cases/rey-frontend/ui-medium/pass/plan/spec.md +5 -0
  125. data/evals/cases/rey-frontend/ui-medium/pass/repo/lib/cart.rb +3 -0
  126. data/evals/cases/rey-frontend/ui-medium/pass/repo/web/cart_badge.js +4 -0
  127. data/evals/cases/rey-frontend/ui-medium/pass/repo/web/cart_badge.test.js +3 -0
  128. data/evals/cases/rey-frontend/ui-medium/pass/run.json +1 -0
  129. data/evals/cases/rey-frontend/ui-medium.yml +55 -0
  130. data/evals/cases/yoda-writer/spec-medium/fail/plan/plan.md +5 -0
  131. data/evals/cases/yoda-writer/spec-medium/fail/plan/spec.md +45 -0
  132. data/evals/cases/yoda-writer/spec-medium/fail/run.json +1 -0
  133. data/evals/cases/yoda-writer/spec-medium/pass/plan/plan.md +0 -0
  134. data/evals/cases/yoda-writer/spec-medium/pass/plan/spec.md +63 -0
  135. data/evals/cases/yoda-writer/spec-medium/pass/run.json +1 -0
  136. data/evals/cases/yoda-writer/spec-medium.yml +40 -0
  137. data/exe/depot +1 -0
  138. data/exe/lanes +1 -0
  139. data/exe/openlanes +32 -0
  140. data/lib/openlanes/adapters/base.rb +69 -0
  141. data/lib/openlanes/adapters/claude.rb +124 -0
  142. data/lib/openlanes/adapters/codex.rb +33 -0
  143. data/lib/openlanes/adapters/model.rb +37 -0
  144. data/lib/openlanes/adapters/pi.rb +22 -0
  145. data/lib/openlanes/adapters/stream.rb +196 -0
  146. data/lib/openlanes/adapters.rb +92 -0
  147. data/lib/openlanes/agents/agent.rb +103 -0
  148. data/lib/openlanes/agents/profile.rb +57 -0
  149. data/lib/openlanes/agents/registry.rb +151 -0
  150. data/lib/openlanes/agents/roster.rb +103 -0
  151. data/lib/openlanes/agents/routing.rb +88 -0
  152. data/lib/openlanes/agents.rb +20 -0
  153. data/lib/openlanes/cli/agents/subcommands/describe.rb +66 -0
  154. data/lib/openlanes/cli/agents/subcommands/list.rb +20 -0
  155. data/lib/openlanes/cli/base.rb +263 -0
  156. data/lib/openlanes/cli/create/create.rb +354 -0
  157. data/lib/openlanes/cli/docs/docs.rb +40 -0
  158. data/lib/openlanes/cli/eval/eval.rb +110 -0
  159. data/lib/openlanes/cli/index/index.rb +40 -0
  160. data/lib/openlanes/cli/init/init.rb +145 -0
  161. data/lib/openlanes/cli/linear/linear.rb +36 -0
  162. data/lib/openlanes/cli/linear/subcommands/import.rb +174 -0
  163. data/lib/openlanes/cli/linear/subcommands/projects.rb +55 -0
  164. data/lib/openlanes/cli/list_plans/list_plans.rb +21 -0
  165. data/lib/openlanes/cli/mail/mail.rb +31 -0
  166. data/lib/openlanes/cli/mail/subcommands/read.rb +43 -0
  167. data/lib/openlanes/cli/mail/subcommands/send.rb +43 -0
  168. data/lib/openlanes/cli/resync/subcommands/dirs.rb +48 -0
  169. data/lib/openlanes/cli/resync/subcommands/prs.rb +111 -0
  170. data/lib/openlanes/cli/run/run.rb +544 -0
  171. data/lib/openlanes/cli/skills/subcommands/install.rb +92 -0
  172. data/lib/openlanes/cli/start/start.rb +110 -0
  173. data/lib/openlanes/cli/state/state.rb +26 -0
  174. data/lib/openlanes/cli/state/subcommands/show.rb +30 -0
  175. data/lib/openlanes/cli/state/subcommands/sign.rb +48 -0
  176. data/lib/openlanes/cli/states/states.rb +15 -0
  177. data/lib/openlanes/cli/unblock/unblock.rb +235 -0
  178. data/lib/openlanes/cli/version/version.rb +13 -0
  179. data/lib/openlanes/cli/web/web.rb +100 -0
  180. data/lib/openlanes/cli/worktree/worktree.rb +85 -0
  181. data/lib/openlanes/cli.rb +162 -0
  182. data/lib/openlanes/engine/board.rb +104 -0
  183. data/lib/openlanes/engine/dispatcher.rb +1297 -0
  184. data/lib/openlanes/engine/progress_log.rb +125 -0
  185. data/lib/openlanes/engine/runner.rb +224 -0
  186. data/lib/openlanes/engine/state_file.rb +190 -0
  187. data/lib/openlanes/engine/tally.rb +177 -0
  188. data/lib/openlanes/engine.rb +19 -0
  189. data/lib/openlanes/evals/case.rb +191 -0
  190. data/lib/openlanes/evals/changes.rb +67 -0
  191. data/lib/openlanes/evals/check.rb +224 -0
  192. data/lib/openlanes/evals/evidence.rb +71 -0
  193. data/lib/openlanes/evals/live.rb +140 -0
  194. data/lib/openlanes/evals/recording.rb +38 -0
  195. data/lib/openlanes/evals/run.rb +34 -0
  196. data/lib/openlanes/evals/score.rb +24 -0
  197. data/lib/openlanes/evals/scorer.rb +42 -0
  198. data/lib/openlanes/evals/verdict.rb +18 -0
  199. data/lib/openlanes/evals.rb +56 -0
  200. data/lib/openlanes/execution/checks.rb +73 -0
  201. data/lib/openlanes/execution/child.rb +228 -0
  202. data/lib/openlanes/execution/clock.rb +121 -0
  203. data/lib/openlanes/execution/control.rb +194 -0
  204. data/lib/openlanes/execution/executor.rb +849 -0
  205. data/lib/openlanes/execution/prompt.rb +455 -0
  206. data/lib/openlanes/execution/transcript.rb +531 -0
  207. data/lib/openlanes/execution.rb +19 -0
  208. data/lib/openlanes/launcher.rb +69 -0
  209. data/lib/openlanes/lifecycle/adoption.rb +197 -0
  210. data/lib/openlanes/lifecycle/brief.rb +288 -0
  211. data/lib/openlanes/lifecycle/completion.rb +58 -0
  212. data/lib/openlanes/lifecycle/creator.rb +126 -0
  213. data/lib/openlanes/lifecycle/lanes.rb +74 -0
  214. data/lib/openlanes/lifecycle/resync.rb +428 -0
  215. data/lib/openlanes/lifecycle/unblocker.rb +236 -0
  216. data/lib/openlanes/lifecycle.rb +28 -0
  217. data/lib/openlanes/linear/api.rb +224 -0
  218. data/lib/openlanes/linear/attribution.rb +185 -0
  219. data/lib/openlanes/linear/fuzzy.rb +68 -0
  220. data/lib/openlanes/linear/import.rb +314 -0
  221. data/lib/openlanes/linear/issue.rb +187 -0
  222. data/lib/openlanes/linear/mapping.rb +116 -0
  223. data/lib/openlanes/linear/push.rb +197 -0
  224. data/lib/openlanes/linear/survey.rb +174 -0
  225. data/lib/openlanes/linear/unit.rb +274 -0
  226. data/lib/openlanes/linear.rb +41 -0
  227. data/lib/openlanes/live/claims.rb +71 -0
  228. data/lib/openlanes/live/command.rb +122 -0
  229. data/lib/openlanes/live/commands.rb +505 -0
  230. data/lib/openlanes/live/keys.rb +93 -0
  231. data/lib/openlanes/live/memory_store.rb +265 -0
  232. data/lib/openlanes/live/meter.rb +51 -0
  233. data/lib/openlanes/live/node.rb +88 -0
  234. data/lib/openlanes/live/publisher.rb +179 -0
  235. data/lib/openlanes/live/repo.rb +60 -0
  236. data/lib/openlanes/live/run_record.rb +133 -0
  237. data/lib/openlanes/live/run_state.rb +101 -0
  238. data/lib/openlanes/live/session.rb +107 -0
  239. data/lib/openlanes/live/snapshot.rb +322 -0
  240. data/lib/openlanes/live/stations.rb +67 -0
  241. data/lib/openlanes/live/store.rb +277 -0
  242. data/lib/openlanes/live.rb +109 -0
  243. data/lib/openlanes/plans/dev_work.rb +56 -0
  244. data/lib/openlanes/plans/feature.rb +131 -0
  245. data/lib/openlanes/plans/index.rb +208 -0
  246. data/lib/openlanes/plans/ledger.rb +255 -0
  247. data/lib/openlanes/plans/mailbox.rb +164 -0
  248. data/lib/openlanes/plans/ordinal.rb +92 -0
  249. data/lib/openlanes/plans/plan_state.rb +291 -0
  250. data/lib/openlanes/plans/pull_request.rb +29 -0
  251. data/lib/openlanes/plans/pull_requests.rb +229 -0
  252. data/lib/openlanes/plans/spec.rb +213 -0
  253. data/lib/openlanes/plans/state_machine.rb +433 -0
  254. data/lib/openlanes/plans/status.rb +365 -0
  255. data/lib/openlanes/plans/subject.rb +133 -0
  256. data/lib/openlanes/plans/tree.rb +79 -0
  257. data/lib/openlanes/plans.rb +39 -0
  258. data/lib/openlanes/presentation/diagram.rb +149 -0
  259. data/lib/openlanes/presentation/documentation.rb +438 -0
  260. data/lib/openlanes/presentation/reporter.rb +181 -0
  261. data/lib/openlanes/presentation/viewer.rb +62 -0
  262. data/lib/openlanes/presentation.rb +23 -0
  263. data/lib/openlanes/skills/installer.rb +68 -0
  264. data/lib/openlanes/skills/skill.rb +60 -0
  265. data/lib/openlanes/skills.rb +25 -0
  266. data/lib/openlanes/support/config.rb +217 -0
  267. data/lib/openlanes/support/frontmatter.rb +38 -0
  268. data/lib/openlanes/support/markdown.rb +56 -0
  269. data/lib/openlanes/support/project.rb +79 -0
  270. data/lib/openlanes/support/ui.rb +671 -0
  271. data/lib/openlanes/vcs/github.rb +171 -0
  272. data/lib/openlanes/vcs/publisher.rb +221 -0
  273. data/lib/openlanes/vcs/worktree.rb +248 -0
  274. data/lib/openlanes/vcs.rb +19 -0
  275. data/lib/openlanes/version.rb +10 -0
  276. data/lib/openlanes/web/app.rb +176 -0
  277. data/lib/openlanes/web/context.rb +66 -0
  278. data/lib/openlanes/web/demo.rb +169 -0
  279. data/lib/openlanes/web/demo_board.rb +286 -0
  280. data/lib/openlanes/web/events.rb +104 -0
  281. data/lib/openlanes/web/launcher.rb +143 -0
  282. data/lib/openlanes/web/server.rb +77 -0
  283. data/lib/openlanes/web/stores.rb +36 -0
  284. data/lib/openlanes/web.rb +92 -0
  285. data/lib/openlanes.rb +151 -0
  286. data/openlanes.gemspec +87 -0
  287. data/schemas/plan-state.schema.json +101 -0
  288. data/skills/openlanes/SKILL.md +48 -0
  289. data/skills/openlanes-documents/SKILL.md +61 -0
  290. data/skills/openlanes-documents/references/blocked-template.md +49 -0
  291. data/skills/openlanes-documents/references/closing-templates.md +59 -0
  292. data/skills/openlanes-documents/references/plan-template.md +61 -0
  293. data/skills/openlanes-documents/references/retroactive-spec.md +43 -0
  294. data/skills/openlanes-documents/references/spec-template.md +45 -0
  295. data/skills/openlanes-run/SKILL.md +90 -0
  296. data/skills/openlanes-setup/SKILL.md +64 -0
  297. data/skills/openlanes-spec/SKILL.md +90 -0
  298. data/skills/openlanes-status/SKILL.md +32 -0
  299. data/skills/openlanes-sync/SKILL.md +22 -0
  300. data/web/dist/assets/apl-B4CMkyY2.js +2 -0
  301. data/web/dist/assets/asciiarmor-Df11BRmG.js +2 -0
  302. data/web/dist/assets/asn1-EdZsLKOL.js +2 -0
  303. data/web/dist/assets/asterisk-B-8jnY81.js +2 -0
  304. data/web/dist/assets/barlow-condensed-latin-600-normal-BFJEwTuo.woff +0 -0
  305. data/web/dist/assets/barlow-condensed-latin-600-normal-DepVgxBB.woff2 +0 -0
  306. data/web/dist/assets/barlow-condensed-latin-ext-600-normal-18ESti3H.woff2 +0 -0
  307. data/web/dist/assets/barlow-condensed-latin-ext-600-normal-Clv9cIcR.woff +0 -0
  308. data/web/dist/assets/barlow-condensed-vietnamese-600-normal-A5AYRdjN.woff2 +0 -0
  309. data/web/dist/assets/barlow-condensed-vietnamese-600-normal-CNlPk46_.woff +0 -0
  310. data/web/dist/assets/brainfuck-C4LP7Hcl.js +2 -0
  311. data/web/dist/assets/clike-B9uivgTg.js +2 -0
  312. data/web/dist/assets/clojure-BMjYHr_A.js +2 -0
  313. data/web/dist/assets/cmake-BQqOBYOt.js +2 -0
  314. data/web/dist/assets/cobol-CWcv1MsR.js +2 -0
  315. data/web/dist/assets/coffeescript-S37ZYGWr.js +2 -0
  316. data/web/dist/assets/commonlisp-DBKNyK5s.js +2 -0
  317. data/web/dist/assets/crystal-SjHAIU92.js +2 -0
  318. data/web/dist/assets/css-BnMrqG3P.js +2 -0
  319. data/web/dist/assets/cypher-C_CwsFkJ.js +2 -0
  320. data/web/dist/assets/d-pRatUO7H.js +2 -0
  321. data/web/dist/assets/diff-DbItnlRl.js +2 -0
  322. data/web/dist/assets/dockerfile-BKs6k2Af.js +2 -0
  323. data/web/dist/assets/dtd-DF_7sFjM.js +2 -0
  324. data/web/dist/assets/dylan-DwRh75JA.js +2 -0
  325. data/web/dist/assets/ebnf-CDyGwa7X.js +2 -0
  326. data/web/dist/assets/ecl-Cabwm37j.js +2 -0
  327. data/web/dist/assets/eiffel-CnydiIhH.js +2 -0
  328. data/web/dist/assets/elm-vLlmbW-K.js +2 -0
  329. data/web/dist/assets/erlang-BNw1qcRV.js +2 -0
  330. data/web/dist/assets/factor-kuTfRLto.js +2 -0
  331. data/web/dist/assets/favicon-C8pAuqqr.png +0 -0
  332. data/web/dist/assets/fcl-Kvtd6kyn.js +2 -0
  333. data/web/dist/assets/forth-Ffai-XNe.js +2 -0
  334. data/web/dist/assets/fortran-DYz_wnZ1.js +2 -0
  335. data/web/dist/assets/gas-Bneqetm1.js +2 -0
  336. data/web/dist/assets/gherkin-heZmZLOM.js +2 -0
  337. data/web/dist/assets/groovy-D9Dt4D0W.js +2 -0
  338. data/web/dist/assets/haskell-Cw1EW3IL.js +2 -0
  339. data/web/dist/assets/haxe-H-WmDvRZ.js +2 -0
  340. data/web/dist/assets/http-DBlCnlav.js +2 -0
  341. data/web/dist/assets/idl-BEugSyMb.js +2 -0
  342. data/web/dist/assets/index-2xPC-eHt.js +2 -0
  343. data/web/dist/assets/index-8dNhRmiO.js +8 -0
  344. data/web/dist/assets/index-B7-O9ujQ.js +2 -0
  345. data/web/dist/assets/index-BdR34cXC.js +2 -0
  346. data/web/dist/assets/index-Bp7W-ZTq.js +2 -0
  347. data/web/dist/assets/index-Byyxbh_z.js +4 -0
  348. data/web/dist/assets/index-C0VmkrO-.js +2 -0
  349. data/web/dist/assets/index-CI74IkWP.js +198 -0
  350. data/web/dist/assets/index-CYxcbpRM.js +3 -0
  351. data/web/dist/assets/index-D0rmJN3h.js +2 -0
  352. data/web/dist/assets/index-DB4QwuYn.js +2 -0
  353. data/web/dist/assets/index-DDI7zoAS.js +2 -0
  354. data/web/dist/assets/index-DGLUuEXi.js +2 -0
  355. data/web/dist/assets/index-OR-odsl9.js +2 -0
  356. data/web/dist/assets/index-P3vSkiU-.js +2 -0
  357. data/web/dist/assets/index-a4EHR-u4.css +1 -0
  358. data/web/dist/assets/index-mRV4XhJZ.js +2 -0
  359. data/web/dist/assets/index-popk9wbW.js +2 -0
  360. data/web/dist/assets/index-uM88Njdj.js +2 -0
  361. data/web/dist/assets/javascript-iXu5QeM3.js +2 -0
  362. data/web/dist/assets/julia-DuME0IfC.js +2 -0
  363. data/web/dist/assets/livescript-BwQOo05w.js +2 -0
  364. data/web/dist/assets/logo-on-dark-DI9gCdN4.png +0 -0
  365. data/web/dist/assets/logo-on-light-BciwqxcJ.png +0 -0
  366. data/web/dist/assets/lua-VAEuO923.js +2 -0
  367. data/web/dist/assets/mathematica-DTrFuWx2.js +2 -0
  368. data/web/dist/assets/mbox-CNhZ1qSd.js +2 -0
  369. data/web/dist/assets/mirc-CjQqDB4T.js +2 -0
  370. data/web/dist/assets/mllike-CXdrOF99.js +2 -0
  371. data/web/dist/assets/modelica-Dc1JOy9r.js +2 -0
  372. data/web/dist/assets/mscgen-BA5vi2Kp.js +2 -0
  373. data/web/dist/assets/mumps-BT43cFF4.js +2 -0
  374. data/web/dist/assets/nginx-DdIZxoE0.js +2 -0
  375. data/web/dist/assets/nsis-LY0VT_u5.js +2 -0
  376. data/web/dist/assets/ntriples-BfvgReVJ.js +2 -0
  377. data/web/dist/assets/octave-Ck1zUtKM.js +2 -0
  378. data/web/dist/assets/overpass-cyrillic-400-normal-CZsFKZCk.woff +0 -0
  379. data/web/dist/assets/overpass-cyrillic-400-normal-tXDxWGRy.woff2 +0 -0
  380. data/web/dist/assets/overpass-cyrillic-600-normal-B-hc8bdn.woff2 +0 -0
  381. data/web/dist/assets/overpass-cyrillic-600-normal-CXB2Mrig.woff +0 -0
  382. data/web/dist/assets/overpass-cyrillic-800-normal-CP6RWG8X.woff2 +0 -0
  383. data/web/dist/assets/overpass-cyrillic-800-normal-PJW4A9xW.woff +0 -0
  384. data/web/dist/assets/overpass-cyrillic-ext-400-normal-AKUz2ZI5.woff2 +0 -0
  385. data/web/dist/assets/overpass-cyrillic-ext-400-normal-C2ZjKngx.woff +0 -0
  386. data/web/dist/assets/overpass-cyrillic-ext-600-normal-CW4Fknsm.woff2 +0 -0
  387. data/web/dist/assets/overpass-cyrillic-ext-600-normal-Zx-mtRSD.woff +0 -0
  388. data/web/dist/assets/overpass-cyrillic-ext-800-normal-CIth-cA7.woff2 +0 -0
  389. data/web/dist/assets/overpass-cyrillic-ext-800-normal-hx3cL3ZN.woff +0 -0
  390. data/web/dist/assets/overpass-latin-400-normal-BpeLJ0bs.woff2 +0 -0
  391. data/web/dist/assets/overpass-latin-400-normal-c_C8zulu.woff +0 -0
  392. data/web/dist/assets/overpass-latin-600-normal-25RhTNCi.woff2 +0 -0
  393. data/web/dist/assets/overpass-latin-600-normal-DhYQrzaY.woff +0 -0
  394. data/web/dist/assets/overpass-latin-800-normal-Bzx_XP5l.woff2 +0 -0
  395. data/web/dist/assets/overpass-latin-800-normal-D6-q4f0l.woff +0 -0
  396. data/web/dist/assets/overpass-latin-ext-400-normal-BIkXRwl6.woff2 +0 -0
  397. data/web/dist/assets/overpass-latin-ext-400-normal-DUq_h-_z.woff +0 -0
  398. data/web/dist/assets/overpass-latin-ext-600-normal-Cyj4Nio0.woff2 +0 -0
  399. data/web/dist/assets/overpass-latin-ext-600-normal-DQKKMGzp.woff +0 -0
  400. data/web/dist/assets/overpass-latin-ext-800-normal-D6WKWGNs.woff +0 -0
  401. data/web/dist/assets/overpass-latin-ext-800-normal-DZ60AB77.woff2 +0 -0
  402. data/web/dist/assets/overpass-mono-cyrillic-400-normal-B144qALP.woff2 +0 -0
  403. data/web/dist/assets/overpass-mono-cyrillic-400-normal-XK9_jwT8.woff +0 -0
  404. data/web/dist/assets/overpass-mono-cyrillic-ext-400-normal-Dgejvo4A.woff2 +0 -0
  405. data/web/dist/assets/overpass-mono-cyrillic-ext-400-normal-DgxwtUZy.woff +0 -0
  406. data/web/dist/assets/overpass-mono-latin-400-normal-DqAOLzCO.woff +0 -0
  407. data/web/dist/assets/overpass-mono-latin-400-normal-VINZG6Js.woff2 +0 -0
  408. data/web/dist/assets/overpass-mono-latin-ext-400-normal-5YIw-E8g.woff +0 -0
  409. data/web/dist/assets/overpass-mono-latin-ext-400-normal-DBdJzBs_.woff2 +0 -0
  410. data/web/dist/assets/overpass-mono-vietnamese-400-normal-DrLp_cHj.woff +0 -0
  411. data/web/dist/assets/overpass-mono-vietnamese-400-normal-jmOeL0rj.woff2 +0 -0
  412. data/web/dist/assets/overpass-vietnamese-400-normal-BKRCEiXy.woff2 +0 -0
  413. data/web/dist/assets/overpass-vietnamese-400-normal-CCWAWVG7.woff +0 -0
  414. data/web/dist/assets/overpass-vietnamese-600-normal-CjS-4vQf.woff2 +0 -0
  415. data/web/dist/assets/overpass-vietnamese-600-normal-DlMMlPfM.woff +0 -0
  416. data/web/dist/assets/overpass-vietnamese-800-normal-BB8K9-Vo.woff2 +0 -0
  417. data/web/dist/assets/overpass-vietnamese-800-normal-DZUygvj_.woff +0 -0
  418. data/web/dist/assets/oz-BzwKVEFT.js +2 -0
  419. data/web/dist/assets/pascal--L3eBynH.js +2 -0
  420. data/web/dist/assets/perl-CdXCOZ3F.js +2 -0
  421. data/web/dist/assets/pig-CevX1Tat.js +2 -0
  422. data/web/dist/assets/powershell-CFHJl5sT.js +2 -0
  423. data/web/dist/assets/properties-C78fOPTZ.js +2 -0
  424. data/web/dist/assets/protobuf-ChK-085T.js +2 -0
  425. data/web/dist/assets/pug-DeIclll2.js +2 -0
  426. data/web/dist/assets/puppet-DMA9R1ak.js +2 -0
  427. data/web/dist/assets/python-BuPzkPfP.js +2 -0
  428. data/web/dist/assets/q-pXgVlZs6.js +2 -0
  429. data/web/dist/assets/r-B6wPVr8A.js +2 -0
  430. data/web/dist/assets/rpm-CTu-6PCP.js +2 -0
  431. data/web/dist/assets/ruby-BK5aLJev.js +2 -0
  432. data/web/dist/assets/sas-B4kiWyti.js +2 -0
  433. data/web/dist/assets/scheme-C41bIUwD.js +2 -0
  434. data/web/dist/assets/shell-CjFT_Tl9.js +2 -0
  435. data/web/dist/assets/sieve-C3Gn_uJK.js +2 -0
  436. data/web/dist/assets/simple-mode-GW_nhZxv.js +2 -0
  437. data/web/dist/assets/smalltalk-CnHTOXQT.js +2 -0
  438. data/web/dist/assets/solr-DehyRSwq.js +2 -0
  439. data/web/dist/assets/sparql-DkYu6x3z.js +2 -0
  440. data/web/dist/assets/spreadsheet-BCZA_wO0.js +2 -0
  441. data/web/dist/assets/sql-D0XecflT.js +2 -0
  442. data/web/dist/assets/stex-C3f8Ysf7.js +2 -0
  443. data/web/dist/assets/stylus-B533Al4x.js +2 -0
  444. data/web/dist/assets/swift-BzpIVaGY.js +2 -0
  445. data/web/dist/assets/tcl-DVfN8rqt.js +2 -0
  446. data/web/dist/assets/textile-CnDTJFAw.js +2 -0
  447. data/web/dist/assets/tiddlywiki-DO-Gjzrf.js +2 -0
  448. data/web/dist/assets/tiki-DGYXhP31.js +2 -0
  449. data/web/dist/assets/toml-Bm5Em-hy.js +2 -0
  450. data/web/dist/assets/troff-wAsdV37c.js +2 -0
  451. data/web/dist/assets/ttcn-CfJYG6tj.js +2 -0
  452. data/web/dist/assets/ttcn-cfg-B9xdYoR4.js +2 -0
  453. data/web/dist/assets/turtle-B1tBg_DP.js +2 -0
  454. data/web/dist/assets/vb-CmGdzxic.js +2 -0
  455. data/web/dist/assets/vbscript-BuJXcnF6.js +2 -0
  456. data/web/dist/assets/velocity-D8B20fx6.js +2 -0
  457. data/web/dist/assets/verilog-C6RDOZhf.js +2 -0
  458. data/web/dist/assets/vhdl-lSbBsy5d.js +2 -0
  459. data/web/dist/assets/webidl-ZXfAyPTL.js +2 -0
  460. data/web/dist/assets/xquery-DzFWVndE.js +2 -0
  461. data/web/dist/assets/yacas-BJ4BC0dw.js +2 -0
  462. data/web/dist/assets/z80-Hz9HOZM7.js +2 -0
  463. data/web/dist/index.html +14 -0
  464. data/web/dist/vite.svg +1 -0
  465. data/web/src/api/fixtures/board.json +512 -0
  466. metadata +907 -0
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module Evals
5
+ # Everything a check may look at: the folder, where it ended, who was
6
+ # asked to write it, what the run spent and what it changed.
7
+ #
8
+ # @!attribute [r] folder
9
+ # @return [String] the plan folder, which may not exist
10
+ # @!attribute [r] state
11
+ # @return [Symbol, nil]
12
+ # @!attribute [r] agent
13
+ # @return [OpenLanes::Agents::Agent]
14
+ # @!attribute [r] run
15
+ # @return [OpenLanes::Evals::Run]
16
+ # @!attribute [r] changes
17
+ # @return [OpenLanes::Evals::Changes, nil] nil when no repository was recorded
18
+ Evidence = Data.define(:folder, :state, :agent, :run, :changes) do
19
+ # @param name [String] a file in the plan folder
20
+ # @return [String, nil]
21
+ def read(name)
22
+ path = File.join(folder, name)
23
+ File.file?(path) ? File.read(path, encoding: "UTF-8") : nil
24
+ end
25
+
26
+ # A document without its ledger blocks, narrowed to one section when
27
+ # asked. The ledger is the harness's bookkeeping, not the agent's prose,
28
+ # and counting it would let a run pad itself by signing.
29
+ #
30
+ # @param name [String]
31
+ # @param section [String, nil] a heading's text
32
+ # @return [String, nil] nil when the file or the section is missing
33
+ def text(name, section = nil)
34
+ body = read(name) or return nil
35
+ body = Plans::Ledger.stripped(body)
36
+ section ? Evidence.section(body, section) : body
37
+ end
38
+
39
+ # The folder as the state machine would see it under +status+'s name,
40
+ # so the status's own invariant can be asked of a folder whose name
41
+ # carries no state, such as a recording's `plan/`.
42
+ #
43
+ # @param status [OpenLanes::Plans::Status]
44
+ # @return [OpenLanes::Plans::Subject]
45
+ def subject_as(status)
46
+ Plans.subject(Plans::Feature.new(ordinal: Plans.ordinal("1"),
47
+ status:,
48
+ slug: "eval",
49
+ dirname: File.basename(folder),
50
+ path: folder))
51
+ end
52
+
53
+ # @param name [String]
54
+ # @return [Regexp] a Markdown heading with that text, any level
55
+ def self.heading(name) = /\A[ \t]{0,3}\#{1,6}[ \t]+#{Regexp.escape(name)}(?!\w)/i
56
+
57
+ # The body under a heading, up to the next heading of the same or a
58
+ # higher level.
59
+ #
60
+ # @param text [String]
61
+ # @param name [String]
62
+ # @return [String, nil]
63
+ def self.section(text, name)
64
+ lines = text.lines
65
+ start = lines.index { |l| l.match?(heading(name)) } or return nil
66
+ level = lines[start][/\A[ \t]*(#+)/, 1].size
67
+ lines.drop(start + 1).take_while { |l| (l[/\A[ \t]{0,3}(#+)[ \t]/, 1]&.size || 7) > level }.join
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "tmpdir"
5
+ require "yaml"
6
+
7
+ module OpenLanes
8
+ module Evals
9
+ # Runs the real agent on a case's fixture, in a repository nobody else
10
+ # uses, under a token cap, and scores what it leaves behind.
11
+ #
12
+ # The steps mirror what the runner does around one agent, and reuse its
13
+ # pieces rather than imitating them: {Execution.executor} runs the agent
14
+ # with the same autonomy boundary, and `resync dirs` renames the folder to
15
+ # the state its contents justify, as the runner does after every round.
16
+ # What it cannot reproduce is publishing. A build agent's 🟢 needs a pull
17
+ # request on GitHub, so build cases check signatures and diffs, not state.
18
+ class Live
19
+ # The default ceiling for one live case. Cheap enough to run by hand,
20
+ # high enough that a deep research case is not killed for doing its job.
21
+ DEFAULT_MAX_TOKENS = 300_000
22
+
23
+ # The one plan in the fixture repository.
24
+ ORDINAL = "001.00"
25
+
26
+ # Who the fixture commit is from: a made-up identity, so a commit in a
27
+ # temp repository can never be mistaken for a person's.
28
+ AUTHOR = ["-c", "user.name=OpenLanes Eval", "-c", "user.email=eval@example.com",
29
+ "-c", "commit.gpgsign=false", "-c", "core.hooksPath=/dev/null"].freeze
30
+
31
+ # @!attribute [r] score
32
+ # @return [OpenLanes::Evals::Score]
33
+ # @!attribute [r] result
34
+ # @return [OpenLanes::Execution::Executor::Result]
35
+ # @!attribute [r] root
36
+ # @return [String, nil] the repository, when it was kept
37
+ Outcome = Data.define(:score, :result, :root)
38
+
39
+ # @param kase [OpenLanes::Evals::Case] at the depth to run it
40
+ # @param registry [OpenLanes::Agents::Registry]
41
+ # @param max_tokens [Integer] enforced by the executor's meter
42
+ # @param timeout [Integer, nil] seconds; nil leaves the agent's own clock
43
+ # @param spawn [#call, nil] the executor's seam; nil starts the real CLI
44
+ # @param trace_dir [String, nil] where the NDJSON trace goes
45
+ # @param keep [Boolean] leave the repository on disk for inspection
46
+ def initialize(kase, registry: Agents.registry, max_tokens: DEFAULT_MAX_TOKENS, timeout: nil, spawn: nil,
47
+ trace_dir: nil, keep: false)
48
+ @kase = kase
49
+ @registry = registry
50
+ @agent = registry.find(kase.agent) or raise Invalid, "#{kase.path}: names no agent called #{kase.agent}"
51
+ @max_tokens = max_tokens
52
+ @timeout = timeout
53
+ @spawn = spawn
54
+ @trace_dir = trace_dir
55
+ @keep = keep
56
+ end
57
+
58
+ # @return [OpenLanes::Evals::Live::Outcome]
59
+ def call
60
+ root = Dir.mktmpdir("openlanes-eval-")
61
+ begin
62
+ plans = build(root)
63
+ result = executor(root).call(@agent, start(plans), root:)
64
+ Lifecycle.resync_dirs(tree: Plans.tree(plans)).call(commit: true)
65
+ found = Plans.tree(plans).find(ORDINAL)
66
+ run = Run.new(seconds: result.seconds, tokens: result.fresh)
67
+ score = Scorer.new(@kase, registry: @registry)
68
+ .call(folder: found&.feature&.path || File.join(plans, "missing"), run:, repo: root)
69
+ Outcome.new(score:, result:, root: @keep ? root : nil)
70
+ ensure
71
+ FileUtils.rm_rf(root) unless @keep
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ # A repository holding the fixture, committed, so the executor's
78
+ # after-check has a HEAD to hold the agent to.
79
+ #
80
+ # @return [String] the `.plans` directory
81
+ def build(root)
82
+ git(root, "init", "-q")
83
+ @kase.fixture.repo.each { |name, body| write(File.join(root, name), body) }
84
+ plans = File.join(root, OpenLanes::PLANS_DIR)
85
+ status = Plans.status(@kase.fixture.state)
86
+ folder = File.join(plans, Plans.plan_dirname(ORDINAL, status, @kase.fixture.slug))
87
+ @kase.fixture.files.each { |name, body| write(File.join(folder, name), body) }
88
+ stamp_depth(File.join(folder, Plans::Spec::FILENAME))
89
+ git(root, "add", "-A")
90
+ git(root, *AUTHOR, "commit", "-q", "-m", "Eval fixture #{@kase}")
91
+ plans
92
+ end
93
+
94
+ # The depth reaches the agent the way it would in a real plan: as
95
+ # `depth:` in spec.md's frontmatter, which the profile turns into effort.
96
+ #
97
+ # @return [void]
98
+ def stamp_depth(path)
99
+ meta, body = Frontmatter.split(File.read(path, encoding: "UTF-8"))
100
+ meta = meta.merge("depth" => @kase.depth.to_s)
101
+ File.write(path, "#{YAML.dump(meta)}---\n#{body}")
102
+ end
103
+
104
+ # The runner moves a folder to the agent's `starts_as` the moment it is
105
+ # dispatched, so r2d2 works a 🟡 plan and hansolo a 👀 one. Same here.
106
+ #
107
+ # @return [OpenLanes::Plans::Subject]
108
+ def start(plans)
109
+ subject = Plans.tree(plans).find(ORDINAL)
110
+ target = @agent.starts_as
111
+ return subject unless target && subject.machine.may?(target)
112
+
113
+ subject.machine.promote!(target)
114
+ Plans.tree(plans).find(ORDINAL)
115
+ end
116
+
117
+ # @return [OpenLanes::Execution::Executor]
118
+ def executor(root)
119
+ options = { root:, max_tokens: @max_tokens, fresh_budget: true, timeout: @timeout }
120
+ options[:spawn] = @spawn if @spawn
121
+ options[:trace_dir] = @trace_dir if @trace_dir
122
+ Execution.executor(**options)
123
+ end
124
+
125
+ # @return [void]
126
+ def write(path, body)
127
+ FileUtils.mkdir_p(File.dirname(path))
128
+ File.write(path, body)
129
+ end
130
+
131
+ # @return [void]
132
+ # @raise [OpenLanes::Error] when git refuses
133
+ def git(root, *args)
134
+ return if system("git", "-C", root, *args, out: File::NULL, err: File::NULL)
135
+
136
+ raise Error, "git #{args.last(2).join(" ")} failed in #{root}"
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module Evals
5
+ # A plan folder as some run of the agent left it, kept so the scorers can
6
+ # be checked without running anything.
7
+ #
8
+ # <case>/pass/plan/ the plan folder
9
+ # <case>/pass/repo/ the repository around it, for coding agents
10
+ # <case>/pass/run.json {"seconds": .., "tokens": .., "state": ..}
11
+ #
12
+ # The folder sits in `plan/` rather than at the top so that `run.json` and
13
+ # `repo/` are never mistaken for files the agent wrote.
14
+ #
15
+ # @!attribute [r] dir
16
+ # @return [String]
17
+ Recording = Data.define(:dir) do
18
+ # @param dir [String]
19
+ # @return [OpenLanes::Evals::Recording, nil] nil when there is no plan folder
20
+ def self.at(dir) = File.directory?(File.join(dir, "plan")) ? new(dir:) : nil
21
+
22
+ # @return [String] the plan folder
23
+ def plan = File.join(dir, "plan")
24
+
25
+ # @return [String, nil] the repository, when one was recorded
26
+ def repo
27
+ path = File.join(dir, "repo")
28
+ File.directory?(path) ? path : nil
29
+ end
30
+
31
+ # @return [OpenLanes::Evals::Run]
32
+ def run = Run.load(File.join(dir, "run.json"))
33
+
34
+ # @return [Symbol] :pass or :fail
35
+ def kind = File.basename(dir).to_sym
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module OpenLanes
6
+ module Evals
7
+ # What one run spent, and where it left the folder.
8
+ #
9
+ # A live run measures all three. A recording carries them in `run.json`,
10
+ # because a directory on disk cannot say how long it took to write.
11
+ #
12
+ # @!attribute [r] seconds
13
+ # @return [Float, nil] wall clock
14
+ # @!attribute [r] tokens
15
+ # @return [Integer, nil] sent plus generated
16
+ # @!attribute [r] state
17
+ # @return [Symbol, nil] the status key the folder ended in, when its
18
+ # name cannot say so itself
19
+ Run = Data.define(:seconds, :tokens, :state) do
20
+ def initialize(seconds: nil, tokens: nil, state: nil)
21
+ super(seconds: seconds&.to_f, tokens: tokens&.to_i, state: state&.to_sym)
22
+ end
23
+
24
+ # @param path [String] a `run.json`
25
+ # @return [OpenLanes::Evals::Run] empty when the file is absent
26
+ def self.load(path)
27
+ return new unless File.file?(path)
28
+
29
+ data = JSON.parse(File.read(path))
30
+ new(seconds: data["seconds"], tokens: data["tokens"], state: data["state"])
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module Evals
5
+ # Every verdict for one case against one folder.
6
+ #
7
+ # @!attribute [r] kase
8
+ # @return [OpenLanes::Evals::Case] at the depth it was scored
9
+ # @!attribute [r] verdicts
10
+ # @return [Array<OpenLanes::Evals::Verdict>]
11
+ Score = Data.define(:kase, :verdicts) do
12
+ # A case with no checks has proved nothing, so it does not pass.
13
+ #
14
+ # @return [Boolean]
15
+ def passed? = !verdicts.empty? && verdicts.all?(&:ok?)
16
+
17
+ # @return [Array<OpenLanes::Evals::Verdict>]
18
+ def failures = verdicts.reject(&:ok?)
19
+
20
+ # @return [String] `7/8`
21
+ def tally = "#{verdicts.count(&:ok?)}/#{verdicts.size}"
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module Evals
5
+ # Applies a case's checks to a folder.
6
+ #
7
+ # The scorer does not care whether the folder was recorded months ago or
8
+ # written a second ago by a live run; both reach it as a path plus a
9
+ # {Run}, which is what lets the recordings test the scorer that the live
10
+ # runs depend on.
11
+ class Scorer
12
+ # @param kase [OpenLanes::Evals::Case]
13
+ # @param registry [OpenLanes::Agents::Registry]
14
+ def initialize(kase, registry: Agents.registry)
15
+ @kase = kase
16
+ @agent = registry.find(kase.agent) or raise Invalid, "#{kase.path}: names no agent called #{kase.agent}"
17
+ end
18
+
19
+ # @param folder [String] the plan folder
20
+ # @param run [OpenLanes::Evals::Run, nil]
21
+ # @param repo [String, nil] the repository afterwards
22
+ # @return [OpenLanes::Evals::Score]
23
+ def call(folder:, run: nil, repo: nil)
24
+ run ||= Run.new
25
+ evidence = Evidence.new(folder: File.expand_path(folder),
26
+ state: state_of(folder, run),
27
+ agent: @agent,
28
+ run:,
29
+ changes: repo && Changes.between(@kase.fixture.repo, repo))
30
+ Score.new(kase: @kase, verdicts: @kase.checks.map { |check| check.call(evidence) })
31
+ end
32
+
33
+ private
34
+
35
+ # A folder named like a plan says where it is; a recording's `plan/`
36
+ # cannot, and leans on what `run.json` says instead.
37
+ #
38
+ # @return [Symbol, nil]
39
+ def state_of(folder, run) = Plans.feature(File.expand_path(folder))&.status&.key || run.state
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module Evals
5
+ # One check's answer.
6
+ #
7
+ # @!attribute [r] name
8
+ # @return [String] the check, as a reader would name it
9
+ # @!attribute [r] ok
10
+ # @return [Boolean]
11
+ # @!attribute [r] detail
12
+ # @return [String] what was found, pass or fail
13
+ Verdict = Data.define(:name, :ok, :detail) do
14
+ # @return [Boolean]
15
+ def ok? = ok
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ # Per-agent evals: did the agent finish the task, and did it work at the
5
+ # depth it was asked for?
6
+ #
7
+ # A case (`evals/cases/<agent>/<id>.yml`) describes a fixture plan, the depth
8
+ # requested and the deterministic checks that hold an agent to both. Offline,
9
+ # the checks score recorded plan folders, a passing and a failing one per
10
+ # case, so the scorers are tested by the same corpus they score. Live, the
11
+ # agent runs in a throwaway repository under a token cap, and the folder it
12
+ # leaves behind is scored the same way.
13
+ module Evals
14
+ # Where the shipped cases live: beside the gem, so `openlanes eval` finds
15
+ # them from any project root.
16
+ DEFAULT_DIR = File.expand_path("../../evals/cases", __dir__)
17
+
18
+ # A case file that cannot be scored as written.
19
+ class Invalid < OpenLanes::Error; end
20
+
21
+ module_function
22
+
23
+ # @param dir [String] a directory of `<agent>/<id>.yml` files
24
+ # @param registry [OpenLanes::Agents::Registry] the agents a case may name
25
+ # @return [Array<OpenLanes::Evals::Case>] ordered by agent, then id
26
+ # @raise [OpenLanes::Evals::Invalid] when any case fails validation
27
+ def cases(dir = DEFAULT_DIR, registry: Agents.registry) = Case.all(dir, registry:)
28
+
29
+ # @param kase [OpenLanes::Evals::Case]
30
+ # @param folder [String] the plan folder as the agent left it
31
+ # @param run [OpenLanes::Evals::Run, nil] what the run spent, and where it ended
32
+ # @param repo [String, nil] the repository as the agent left it, for the
33
+ # changed-path checks
34
+ # @param registry [OpenLanes::Agents::Registry]
35
+ # @return [OpenLanes::Evals::Score]
36
+ def score(kase, folder:, run: nil, repo: nil, registry: Agents.registry)
37
+ Scorer.new(kase, registry:).call(folder:, run:, repo:)
38
+ end
39
+
40
+ # @param kase [OpenLanes::Evals::Case]
41
+ # @param kind [Symbol] :pass or :fail
42
+ # @param registry [OpenLanes::Agents::Registry]
43
+ # @return [OpenLanes::Evals::Score, nil] nil when the case ships no such recording
44
+ def score_recording(kase, kind = :pass, registry: Agents.registry)
45
+ recording = kase.recording(kind) or return nil
46
+
47
+ score(kase, folder: recording.plan, run: recording.run, repo: recording.repo, registry:)
48
+ end
49
+
50
+ # Runs the real agent. Nothing calls this unless `--live` was typed.
51
+ #
52
+ # @param kase [OpenLanes::Evals::Case]
53
+ # @return [OpenLanes::Evals::Live::Outcome]
54
+ def run_live(kase, **) = Live.new(kase, **).call
55
+ end
56
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module Execution
5
+ # The requirement every agent that changes code carries whether or not the
6
+ # spec says it: after the changes, every test suite and linter passes.
7
+ #
8
+ # A spec author writes the things that are particular to a plan. That the
9
+ # suite stays green is particular to none of them, so it was written in
10
+ # none of them, and a builder that never ran the linter shipped work the
11
+ # reviewer then had to reject for it. Deriving the command from the
12
+ # checkout, rather than asking each spec to name it, keeps it one fact in
13
+ # one place: the project's own justfile or Gemfile.
14
+ module Checks
15
+ # Agents whose work is code, or the judging of code. Research, planning
16
+ # and unblocking write documents, and running a suite for a document is
17
+ # minutes spent proving nothing.
18
+ PHASES = %w[build frontend review].freeze
19
+
20
+ # The recipe a project names its everything-gate with.
21
+ RECIPE = "check-all"
22
+
23
+ JUSTFILES = %w[justfile Justfile .justfile].freeze
24
+
25
+ # A recipe header: optional `@`, the name, optional parameters, a colon
26
+ # that is not the start of `:=` (an assignment).
27
+ RECIPE_LINE = /^@?#{Regexp.escape(RECIPE)}(?:\s[^:\n]*)?:(?!=)/
28
+
29
+ RUBY = ["bundle exec rspec", "bundle exec rubocop"].freeze
30
+
31
+ class << self
32
+ # @param agent [OpenLanes::Agents::Agent]
33
+ # @return [Boolean] whether the agent carries the requirement
34
+ def apply_to?(agent) = PHASES.include?(agent.phase.to_s)
35
+
36
+ # @param root [String] the checkout the agent works in
37
+ # @return [Array<String>] the commands that must pass, empty when the
38
+ # project names none this can recognise
39
+ def commands(root)
40
+ return ["just #{RECIPE}"] if recipe?(root)
41
+ return RUBY if File.file?(File.join(root, "Gemfile"))
42
+
43
+ []
44
+ end
45
+
46
+ # @param root [String]
47
+ # @return [String] the requirement, one line
48
+ def requirement(root)
49
+ commands = commands(root)
50
+ if commands.empty?
51
+ "After your changes, every test suite and linter the project defines passes. " \
52
+ "Find them in its README, AGENTS.md or CLAUDE.md, run them all, and fix what they report."
53
+ else
54
+ "After your changes, all tests and linters pass: #{commands.map { "`#{it}`" }.join(" and ")} " \
55
+ "#{commands.size == 1 ? "exits" : "each exit"} 0. Run #{commands.size == 1 ? "it" : "them"} " \
56
+ "before you sign Completed, and fix what they report rather than reporting it."
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ # @param root [String]
63
+ # @return [Boolean]
64
+ def recipe?(root)
65
+ JUSTFILES.any? do |name|
66
+ path = File.join(root, name)
67
+ File.file?(path) && File.read(path, encoding: "UTF-8").match?(RECIPE_LINE)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end