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,45 @@
1
+ # Cart Quantity Limits
2
+
3
+ Customers of Example Corp's shop can add negative or absurd quantities to a cart.
4
+
5
+ ## What research needs to settle
6
+
7
+ 1. Where is quantity validated today?
8
+
9
+ ## Research
10
+
11
+ 1. Nowhere. `lib/cart.rb:4` stores whatever `add` receives.
12
+ 2. The checkout service rejects quantities above 99 with a 422 (`lib/checkout.rb:12`).
13
+
14
+ ### Could not settle
15
+
16
+ 1. Whether zero should remove the line or be rejected.
17
+
18
+ ## Goal
19
+
20
+ Validate quantities in the cart.
21
+
22
+ ## Non-Goals
23
+
24
+ - Changing the checkout limit.
25
+
26
+ ## In scope
27
+
28
+ - Handles errors.
29
+
30
+ ## Out of scope
31
+
32
+ - Bulk orders.
33
+
34
+ ## Back end / front end
35
+
36
+ All back end.
37
+
38
+ ## Open questions
39
+
40
+ None.
41
+
42
+ > [!NOTE]
43
+ >
44
+ > [2026-09-04 12:02:11 PM PDT] [ agent: yoda-writer status: Started, round 1 ]
45
+ > [2026-09-04 12:04:02 PM PDT] [ agent: yoda-writer status: Completed, round 1 ]
@@ -0,0 +1 @@
1
+ {"seconds": 111, "tokens": 21900, "state": "ready_for_planning"}
@@ -0,0 +1,63 @@
1
+ # Cart Quantity Limits
2
+
3
+ Customers of Example Corp's shop can add negative or absurd quantities to a cart.
4
+
5
+ ## What research needs to settle
6
+
7
+ 1. Where is quantity validated today?
8
+
9
+ ## Research
10
+
11
+ 1. Nowhere. `lib/cart.rb:4` stores whatever `add` receives.
12
+ 2. The checkout service rejects quantities above 99 with a 422 (`lib/checkout.rb:12`).
13
+
14
+ ### Could not settle
15
+
16
+ 1. Whether zero should remove the line or be rejected.
17
+
18
+ ## Goal
19
+
20
+ A cart can only ever hold quantities the checkout will accept, so a customer learns about a bad quantity when they type it rather than at payment. Framed two ways, as a validation problem in `Cart#add` or as a contract shared with checkout, the second wins: the limit already lives in `lib/checkout.rb:12`, and copying it would let the two drift.
21
+
22
+ ## Non-Goals
23
+
24
+ - Changing the checkout limit of 99.
25
+ - Per-product stock limits; stock is a separate system.
26
+ - Changing how the cart is persisted.
27
+ - Localising the error message beyond English.
28
+
29
+ ## In scope
30
+
31
+ - `Cart#add` with a quantity below 1 raises `Cart::InvalidQuantity` and leaves the cart unchanged; a spec proves it.
32
+ - `Cart#add` with a quantity above the checkout limit raises the same error; the limit is read from `Checkout::MAX_QUANTITY`.
33
+ - The cart page shows the error next to the quantity field, and the field keeps the rejected value.
34
+
35
+ ## Out of scope
36
+
37
+ - Bulk orders above 99: they need a sales contact flow, which is a product decision.
38
+ - Backfilling existing carts that already hold bad quantities: none exist in production per the research.
39
+
40
+ ## Back end / front end
41
+
42
+ | Item | Half |
43
+ | --- | --- |
44
+ | `Cart#add` validation and the shared constant | luke-backend |
45
+ | Error display on the cart page | rey-frontend |
46
+
47
+ ## Open questions
48
+
49
+ 1. Whether zero removes the line or is rejected (leah could not settle it). Assumed: rejected.
50
+
51
+ ## Risks
52
+
53
+ - The checkout constant is private today; making it public touches a file another team owns.
54
+
55
+ ## Conclusion
56
+
57
+ When this ships, a cart cannot hold a quantity checkout would refuse, the limit is defined once, and the customer sees the problem on the cart page.
58
+
59
+ > [!NOTE]
60
+ >
61
+ > [2026-09-04 12:02:11 PM PDT] [ agent: yoda-writer status: Started, round 1 ]
62
+ > [2026-09-04 12:09:40 PM PDT] [ agent: yoda-writer status: Completed, round 1 ]
63
+ > [2026-09-04 12:09:41 PM PDT] [ next: palpatine-planner ]
@@ -0,0 +1 @@
1
+ {"seconds": 449, "tokens": 96200, "state": "ready_for_planning"}
@@ -0,0 +1,40 @@
1
+ id: spec-medium
2
+ agent: yoda-writer
3
+ depth: medium
4
+ description: >
5
+ A researched spec. yoda must write every section of a full specification,
6
+ at least three Non-Goals, and leave plan.md blank for the planner; a
7
+ heading in plan.md would claim the plan was already written.
8
+ fixture:
9
+ state: researched
10
+ slug: cart-quantity-limits
11
+ files:
12
+ spec.md: |
13
+ # Cart Quantity Limits
14
+
15
+ Customers of Example Corp's shop can add negative or absurd quantities to a cart.
16
+
17
+ ## What research needs to settle
18
+
19
+ 1. Where is quantity validated today?
20
+
21
+ ## Research
22
+
23
+ 1. Nowhere. `lib/cart.rb:4` stores whatever `add` receives.
24
+ 2. The checkout service rejects quantities above 99 with a 422 (`lib/checkout.rb:12`).
25
+
26
+ ### Could not settle
27
+
28
+ 1. Whether zero should remove the line or be rejected.
29
+ expect:
30
+ state: ready_for_planning
31
+ signed: true
32
+ files_exist: [plan.md]
33
+ sections:
34
+ spec.md: [Goal, Non-Goals, In scope, Out of scope, Back end / front end, Open questions, Risks, Conclusion]
35
+ depth:
36
+ medium:
37
+ words: { file: spec.md, min: 250, max: 2500 }
38
+ count: { file: spec.md, section: Non-Goals, pattern: '^[ \t]*(?:[-*]|\d+\.)[ \t]', min: 3, max: 8 }
39
+ max_seconds: 900
40
+ max_tokens: 150000
data/exe/depot ADDED
@@ -0,0 +1 @@
1
+ openlanes
data/exe/lanes ADDED
@@ -0,0 +1 @@
1
+ openlanes
data/exe/openlanes ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # openlanes — spec → plan → build.
5
+ #
6
+ # Every command does the work it is named for; -n / --dry-run previews it.
7
+ #
8
+ # © 2026 Konstantin Gredeskoul
9
+
10
+ # Resolve the dependencies from this repository without the bundler, thank you very much.
11
+ module OpenLanes
12
+ PROJECT_ROOT = File.expand_path("..", __dir__)
13
+ end
14
+
15
+ if OpenLanes::PROJECT_ROOT.include?("lib/ruby/gems")
16
+ require "rubygems"
17
+ gem "openlanes"
18
+ end
19
+
20
+ # lib/ goes first on the load path: several files `require "openlanes/..."`,
21
+ # and without this those resolve to whichever openlanes gem is installed,
22
+ # not to this checkout, and one class ends up defined twice.
23
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
24
+
25
+ require_relative "../lib/openlanes"
26
+ require_relative "../lib/openlanes/launcher"
27
+
28
+ begin
29
+ OpenLanes::Launcher.new(ARGV.dup).execute!
30
+ rescue SystemExit => e
31
+ exit(e.status)
32
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module Adapters
5
+ # What every adapter answers. A subclass overrides {#argv}; the rest has a
6
+ # default that is right for a CLI with no opinions of its own.
7
+ class Base
8
+ # @return [String] the name agent frontmatter uses
9
+ def name = self.class.name.split("::").last.downcase
10
+
11
+ # @return [String] the executable, for messages
12
+ def executable = name
13
+
14
+ # @param invocation [OpenLanes::Adapters::Invocation]
15
+ # @return [Array<String>]
16
+ def argv(invocation) = raise NotImplementedError, "#{self.class} must build an argv for #{invocation.class}"
17
+
18
+ # A parser for what this CLI prints, with the same interface as
19
+ # {OpenLanes::Execution::Transcript}.
20
+ #
21
+ # @param trace [String, nil] where to keep every line verbatim
22
+ # @return [#push, #finish, #up, #down, #failed?]
23
+ def transcript(trace: nil, &) = Stream.new(trace:, &)
24
+
25
+ # The model to ask for, after any ceiling this CLI imposes.
26
+ #
27
+ # @param model [String, nil]
28
+ # @return [String, nil]
29
+ def model(model) = model
30
+
31
+ # @param effort [String, nil] one of {EFFORTS}
32
+ # @return [String, nil] this CLI's word for it
33
+ def effort(effort) = effort
34
+
35
+ # What the child process gets on top of the harness's own environment.
36
+ #
37
+ # @return [Hash{String => String}]
38
+ def environment = {}
39
+
40
+ # The models this CLI offers, most capable first, for the page's
41
+ # dropdowns and the cost meter. Empty for a CLI nobody has priced.
42
+ #
43
+ # @return [Array<OpenLanes::Adapters::Model>]
44
+ def models = []
45
+
46
+ # @param name [String, nil] an alias or a full id
47
+ # @return [OpenLanes::Adapters::Model, nil]
48
+ def model_entry(name) = models.find { |m| m.id == name.to_s }
49
+
50
+ # What, sent into a resumed session, makes this CLI compact it (W28);
51
+ # nil for a CLI that cannot resume one, whose agents are therefore never
52
+ # compacted mid-round.
53
+ #
54
+ # @return [String, nil]
55
+ def compact_prompt = nil
56
+
57
+ # @return [Boolean] whether an agent on this CLI can be compacted and
58
+ # carried on in the same session
59
+ def compacts? = !compact_prompt.nil?
60
+
61
+ # Whether the CLI itself refuses the denied tools, rather than only being
62
+ # asked not to use them. The prompt says which, because telling an agent
63
+ # a guard exists when it does not is how it stops being careful.
64
+ #
65
+ # @return [Boolean]
66
+ def enforces_tool_denial? = false
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module Adapters
5
+ # `claude -p`, streaming JSON.
6
+ class Claude < Base
7
+ # Models by capability, cheapest first. Anything ranked above
8
+ # {CEILING} is asked for as the ceiling instead: a run that quietly
9
+ # spends Fable money on a task Opus does fine is a run nobody budgeted.
10
+ LADDER = %w[haiku sonnet opus fable].freeze
11
+
12
+ # The most expensive model any agent may run on.
13
+ CEILING = "opus"
14
+
15
+ # Every Claude model priced, most capable first, newest first within a
16
+ # tier. The one place a model id, its label and its rates are written:
17
+ # the dropdown, the cost meter and alias resolution all read it, through
18
+ # {#models}, which offers only what {CEILING} lets run. Rates are base
19
+ # dollars per million tokens, in and out, from
20
+ # https://platform.claude.com/docs/en/about-claude/pricing as read on
21
+ # 2026-10-13. Haiku 5.5 is priced for prompts up to 100k tokens; above
22
+ # that it is $0.50 in and $2.50 out, which this table does not model.
23
+ CATALOGUE = [
24
+ Model.new(id: "claude-fable-5-1", label: "Fable 5.1", rate_in: 10, rate_out: 50),
25
+ Model.new(id: "claude-opus-5-5", label: "Opus 5.5", rate_in: 4, rate_out: 20),
26
+ Model.new(id: "claude-opus-5", label: "Opus 5", rate_in: 5, rate_out: 25),
27
+ Model.new(id: "claude-sonnet-5-5", label: "Sonnet 5.5", rate_in: 2, rate_out: 10),
28
+ Model.new(id: "claude-sonnet-5", label: "Sonnet 5", rate_in: 2, rate_out: 10),
29
+ Model.new(id: "claude-haiku-5-5", label: "Haiku 5.5", rate_in: 0.10, rate_out: 0.50)
30
+ ].freeze
31
+
32
+ # What an agent may actually run on: the catalogue below {CEILING}. A
33
+ # model the CLI would be asked for as something cheaper is not offered,
34
+ # so the page never shows a choice that will not happen.
35
+ MODELS = CATALOGUE.select { |m| LADDER.index { |tier| m.id.include?(tier) } <= LADDER.index(CEILING) }.freeze
36
+
37
+ # The tier `claude` runs when an agent names no model, so an unnamed
38
+ # model is still priced rather than shown as free.
39
+ UNNAMED = "sonnet"
40
+
41
+ # The variable `claude` reads its autocompact threshold from, as a
42
+ # percentage. Found in the 2.1.x binary, where `Vle()` reads it with
43
+ # `parseFloat(process.env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE)`.
44
+ AUTOCOMPACT_VARIABLE = "CLAUDE_AUTOCOMPACT_PCT_OVERRIDE"
45
+
46
+ # Flags that keep the operator's personal setup out of the agent. The
47
+ # project's CLAUDE.md and settings still load. Measured on a one-word
48
+ # prompt: 143 tools, 60 MCP servers, 358 skills and 17 hook events
49
+ # without these; 28 tools and none of the rest with them, at three
50
+ # quarters of the context and half the start-up time.
51
+ LEAN = ["--setting-sources", "project,local",
52
+ "--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}',
53
+ "--disable-slash-commands"].freeze
54
+
55
+ # The one flag of {LEAN} a compaction drops: with it, `claude -p
56
+ # --resume <id> "/compact"` answers "/compact isn't available in this
57
+ # environment" and compacts nothing (measured on 2.1.x).
58
+ SLASH_COMMANDS_OFF = "--disable-slash-commands"
59
+
60
+ # @param invocation [OpenLanes::Adapters::Invocation]
61
+ # @return [Array<String>]
62
+ def argv(invocation)
63
+ # `--include-partial-messages` is what the token meter runs on. Without
64
+ # it the stream reports a settled input count and a placeholder output
65
+ # count — 2 for a four-thousand-token answer — and a spinner counting
66
+ # what came back would read zero all run.
67
+ argv = ["claude", "-p", invocation.prompt, "--add-dir", invocation.root,
68
+ "--output-format", "stream-json", "--verbose", "--include-partial-messages", "--brief"]
69
+ argv += ["--resume", invocation.resume] if invocation.resume
70
+ argv += LEAN.reject { invocation.slash_commands && it == SLASH_COMMANDS_OFF } if invocation.lean
71
+ argv += ["--disallowedTools", invocation.denied_tools.join(",")] unless invocation.denied_tools.empty?
72
+ argv += ["--allowedTools", invocation.allowed_tools.join(",")] unless invocation.allowed_tools.empty?
73
+ model = model(invocation.model)
74
+ argv += ["--model", model] if model
75
+ argv += ["--effort", invocation.effort] if invocation.effort
76
+ argv
77
+ end
78
+
79
+ # @return [OpenLanes::Execution::Transcript]
80
+ def transcript(trace: nil, &) = Execution::Transcript.new(trace:, &)
81
+
82
+ # @param model [String, nil] an alias (`sonnet`) or a full id
83
+ # (`claude-fable-5-1`)
84
+ # @return [String, nil]
85
+ def model(model)
86
+ return model if model.nil? || model.to_s.empty?
87
+
88
+ rank = LADDER.index { |tier| model.to_s.include?(tier) }
89
+ rank && rank > LADDER.index(CEILING) ? CEILING : model.to_s
90
+ end
91
+
92
+ # @return [Boolean]
93
+ def enforces_tool_denial? = true
94
+
95
+ # `claude -p --resume <id> "/compact"` compacts the session and prints a
96
+ # `compact_boundary` event; the agent cannot run it on itself, because
97
+ # `/compact` is the caller's command, not a tool.
98
+ #
99
+ # @return [String]
100
+ def compact_prompt = "/compact"
101
+
102
+ # @return [Hash{String => String}]
103
+ def environment = { AUTOCOMPACT_VARIABLE => AUTOCOMPACT.to_s }
104
+
105
+ # @return [Array<OpenLanes::Adapters::Model>]
106
+ def models = MODELS
107
+
108
+ # A full id as itself; an alias that is exactly a tier (`opus`) as the
109
+ # newest model of that tier, the first in {MODELS} to carry its name;
110
+ # no name as {UNNAMED}. Anything else, `not-opus` or `sonnet-old`
111
+ # included, is no model at all.
112
+ #
113
+ # @param name [String, nil]
114
+ # @return [OpenLanes::Adapters::Model, nil]
115
+ def model_entry(name)
116
+ text = name.to_s
117
+ text = UNNAMED if text.empty? || text == "default"
118
+ return MODELS.find { |m| m.id.match?(/\Aclaude-#{text}-/) } if LADDER.include?(text)
119
+
120
+ MODELS.find { |m| m.id == text }
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module Adapters
5
+ # `codex exec --json`, inside codex's own workspace-write sandbox.
6
+ #
7
+ # The sandbox has no network unless the agent declares `network: true`,
8
+ # which is the same default the Claude adapter gets from withholding
9
+ # WebFetch and WebSearch. Codex has no per-command denylist, so the
10
+ # forbidden commands are stated in the prompt and the after-check (HEAD
11
+ # unmoved, no new remote ref) is what actually holds the line.
12
+ class Codex < Base
13
+ # Codex says `xhigh` for the top of its scale and has no `max`.
14
+ EFFORT = { "low" => "low", "medium" => "medium", "high" => "high", "xhigh" => "xhigh", "max" => "xhigh" }.freeze
15
+
16
+ # @param invocation [OpenLanes::Adapters::Invocation]
17
+ # @return [Array<String>]
18
+ def argv(invocation)
19
+ argv = ["codex", "exec", "--json", "--sandbox", "workspace-write", "--cd", invocation.root]
20
+ argv << "--ignore-user-config" if invocation.lean
21
+ argv += ["--model", invocation.model] if invocation.model
22
+ effort = effort(invocation.effort)
23
+ argv += ["--config", %(model_reasoning_effort="#{effort}")] if effort
24
+ argv += ["--config", "sandbox_workspace_write.network_access=true"] if invocation.network
25
+ argv << invocation.prompt
26
+ end
27
+
28
+ # @param effort [String, nil]
29
+ # @return [String, nil]
30
+ def effort(effort) = effort && EFFORT[effort.to_s]
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module Adapters
5
+ # One model an adapter offers: what the page lists in a car's dropdown,
6
+ # and what a token costs on it.
7
+ #
8
+ # @!attribute [r] id
9
+ # @return [String] the full model id, e.g. `claude-sonnet-5-5`
10
+ # @!attribute [r] label
11
+ # @return [String] e.g. `Sonnet 5.5`
12
+ # @!attribute [r] rate_in
13
+ # @return [Numeric] dollars per million tokens sent
14
+ # @!attribute [r] rate_out
15
+ # @return [Numeric] dollars per million tokens generated
16
+ # @!attribute [r] context
17
+ # @return [Integer] tokens the model's context window holds, the
18
+ # ceiling a car's context meter is drawn against
19
+ Model = Data.define(:id, :label, :rate_in, :rate_out, :context) do
20
+ def initialize(context: Model::WINDOW, **rest) = super
21
+
22
+ # @param up [Integer] tokens in
23
+ # @param down [Integer] tokens out
24
+ # @return [Float] dollars
25
+ def cost(up, down) = ((up.to_i * rate_in) + (down.to_i * rate_out)) / 1_000_000.0
26
+
27
+ # The `ModelChoice` of `web/src/api/types.ts`.
28
+ #
29
+ # @return [Hash]
30
+ def wire = { id:, label:, rates: { in: rate_in, out: rate_out } }
31
+ end
32
+
33
+ # The context window a model gets when its entry names none: every
34
+ # model the adapters price today holds this many tokens.
35
+ Model::WINDOW = 200_000
36
+ end
37
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module Adapters
5
+ # `pi -p --mode json`. Pi has no sandbox and no per-command denylist, so
6
+ # the after-check is the only guard, and the prompt says as much.
7
+ class Pi < Base
8
+ # Pi's `--thinking` scale already uses these words.
9
+ THINKING = Adapters::EFFORTS
10
+
11
+ # @param invocation [OpenLanes::Adapters::Invocation]
12
+ # @return [Array<String>]
13
+ def argv(invocation)
14
+ argv = ["pi", "--print", "--mode", "json", "--no-session"]
15
+ argv += ["--no-extensions", "--no-skills", "--no-prompt-templates"] if invocation.lean
16
+ argv += ["--model", invocation.model] if invocation.model
17
+ argv += ["--thinking", invocation.effort] if THINKING.include?(invocation.effort.to_s)
18
+ argv << invocation.prompt
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module OpenLanes
6
+ module Adapters
7
+ # A transcript for a CLI that prints JSON lines in a schema this harness
8
+ # was not written against: `codex exec --json`, `pi --mode json`.
9
+ #
10
+ # It answers everything {OpenLanes::Execution::Transcript} answers, but
11
+ # it reads events by shape rather than by name. Any `usage` object with
12
+ # `input_tokens` and `output_tokens` moves the meter; any event whose
13
+ # type mentions a tool, a command or an exec counts as a tool call; any
14
+ # event typed as an error or a failure is the error. A CLI that renames
15
+ # its events keeps working, and one that stops reporting usage shows a
16
+ # meter at zero rather than a crash.
17
+ class Stream
18
+ # @param trace [String, nil] where every line is kept verbatim
19
+ # @yieldparam progress [OpenLanes::Execution::Transcript::Progress]
20
+ def initialize(trace: nil, &on_progress)
21
+ @on_progress = on_progress
22
+ @buffer = +""
23
+ @plain = []
24
+ @trace_path = trace
25
+ @trace = trace && File.open(trace, "a")
26
+ @up = 0
27
+ @down = 0
28
+ @cached = 0
29
+ @tools = 0
30
+ @activity = nil
31
+ @result = nil
32
+ @error = nil
33
+ end
34
+
35
+ # @return [Integer] tokens sent, cached input included
36
+ attr_reader :up
37
+
38
+ # @return [Integer] tokens generated
39
+ attr_reader :down
40
+
41
+ # @return [Integer] of {#up}, input the CLI reports as served from cache
42
+ attr_reader :cached
43
+
44
+ # @return [Integer]
45
+ attr_reader :tools
46
+
47
+ # @return [String, nil]
48
+ attr_reader :activity
49
+
50
+ # @return [String, nil] the last thing the agent said
51
+ attr_reader :result
52
+
53
+ # @return [Array<String>] lines that were not JSON
54
+ attr_reader :plain
55
+
56
+ # @return [String, nil]
57
+ attr_reader :error
58
+
59
+ # @return [String, nil]
60
+ attr_reader :trace_path
61
+
62
+ # @return [Integer, nil]
63
+ attr_accessor :pid
64
+
65
+ # These CLIs do not report sub-agents.
66
+ #
67
+ # @return [Integer]
68
+ def spawned = 0
69
+
70
+ # @return [Integer]
71
+ def delegated = 0
72
+
73
+ # @return [String, nil] nothing is sent through a side channel
74
+ def message = nil
75
+
76
+ # @return [Integer] new input plus output
77
+ def fresh = up - cached + down
78
+
79
+ # @return [Boolean]
80
+ def failed? = !@error.nil?
81
+
82
+ # @return [OpenLanes::Execution::Transcript::Progress]
83
+ def progress = Execution::Transcript::Progress.new(activity:, up:, down:, subagents: 0, pid:, message: nil)
84
+
85
+ # @param chunk [String, nil]
86
+ # @return [void]
87
+ def push(chunk)
88
+ return if chunk.nil?
89
+
90
+ @buffer << chunk
91
+ while (index = @buffer.index("\n"))
92
+ consume(@buffer.slice!(0..index).chomp)
93
+ end
94
+ end
95
+
96
+ # @return [void]
97
+ def finish
98
+ consume(@buffer.slice!(0..-1).to_s)
99
+ @trace&.close
100
+ @trace = nil
101
+ end
102
+
103
+ private
104
+
105
+ # @param line [String]
106
+ # @return [void]
107
+ def consume(line)
108
+ text = line.strip
109
+ return if text.empty?
110
+
111
+ @trace&.write("#{text}\n")
112
+ @trace&.flush
113
+ event = text.start_with?("{") ? parse(text) : nil
114
+ return @plain << text if event.nil?
115
+
116
+ handle(event)
117
+ end
118
+
119
+ # @param text [String]
120
+ # @return [Hash, nil]
121
+ def parse(text)
122
+ value = JSON.parse(text)
123
+ value.is_a?(Hash) ? value : nil
124
+ rescue JSON::ParserError
125
+ nil
126
+ end
127
+
128
+ # @param event [Hash]
129
+ # @return [void]
130
+ def handle(event)
131
+ type = event["type"].to_s
132
+ meter(event)
133
+ @tools += 1 if type.match?(/tool|command|exec/i) && type.match?(/start|call|begin/i)
134
+ @error = describe_error(event) if type.match?(/error|fail/i)
135
+ said = text_in(event)
136
+ if said
137
+ @activity = said.lines.first.to_s.strip[0, Execution::Transcript::LIMIT]
138
+ @result = said
139
+ end
140
+ @on_progress&.call(progress)
141
+ end
142
+
143
+ # Usage counts arrive per turn, so they are added rather than replaced.
144
+ #
145
+ # @param event [Hash]
146
+ # @return [void]
147
+ def meter(event)
148
+ usage = find(event, "usage")
149
+ return unless usage.is_a?(Hash)
150
+
151
+ @up += usage["input_tokens"].to_i
152
+ @down += usage["output_tokens"].to_i
153
+ @cached += usage["cached_input_tokens"].to_i
154
+ end
155
+
156
+ # @param event [Hash]
157
+ # @return [String]
158
+ def describe_error(event)
159
+ (find(event, "message") || find(event, "error") || event["type"]).to_s
160
+ end
161
+
162
+ # The agent's own words, wherever this CLI put them.
163
+ #
164
+ # @param event [Hash]
165
+ # @return [String, nil]
166
+ def text_in(event)
167
+ text = find(event, "text")
168
+ text.is_a?(String) && !text.strip.empty? ? text : nil
169
+ end
170
+
171
+ # Depth-first search for the first value under +key+.
172
+ #
173
+ # @param node [Object]
174
+ # @param key [String]
175
+ # @return [Object, nil]
176
+ def find(node, key)
177
+ case node
178
+ when Hash
179
+ return node[key] if node.key?(key)
180
+
181
+ node.each_value do |value|
182
+ found = find(value, key)
183
+ return found unless found.nil?
184
+ end
185
+ nil
186
+ when Array
187
+ node.each do |value|
188
+ found = find(value, key)
189
+ return found unless found.nil?
190
+ end
191
+ nil
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end