openlanes 3.0.1 → 3.1.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 (319) hide show
  1. checksums.yaml +4 -4
  2. data/Brewfile +42 -0
  3. data/agents/bb8-resolver.md +49 -0
  4. data/agents/c3po-operator.md +4 -0
  5. data/agents/chewie-verifier.md +85 -0
  6. data/agents/hansolo-reviewer.md +20 -15
  7. data/agents/lando-broker.md +81 -19
  8. data/agents/leah-researcher.md +6 -6
  9. data/agents/luke-backend.md +41 -20
  10. data/agents/{palpatine-planner.md → luthen-rael-planner.md} +15 -13
  11. data/agents/palpatine-risk-assessor.md +112 -0
  12. data/agents/r2d2-mechanic.md +5 -1
  13. data/agents/rey-frontend.md +38 -19
  14. data/agents/yoda-writer.md +7 -7
  15. data/evals/cases/chewie-verifier/verify-medium/fail/plan/plan.md +7 -0
  16. data/evals/cases/chewie-verifier/verify-medium/fail/plan/pull-requests.md +10 -0
  17. data/evals/cases/chewie-verifier/verify-medium/fail/plan/spec.md +5 -0
  18. data/evals/cases/chewie-verifier/verify-medium/fail/plan/verification/1/final.png +0 -0
  19. data/evals/cases/chewie-verifier/verify-medium/fail/plan/verification/1/report.md +7 -0
  20. data/evals/cases/chewie-verifier/verify-medium/fail/repo/bin/greet +1 -0
  21. data/evals/cases/chewie-verifier/verify-medium/fail/run.json +1 -0
  22. data/evals/cases/chewie-verifier/verify-medium/pass/plan/plan.md +7 -0
  23. data/evals/cases/chewie-verifier/verify-medium/pass/plan/pull-requests.md +10 -0
  24. data/evals/cases/chewie-verifier/verify-medium/pass/plan/spec.md +5 -0
  25. data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/final.png +0 -0
  26. data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/report.md +14 -0
  27. data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/step-01.png +0 -0
  28. data/evals/cases/chewie-verifier/verify-medium/pass/repo/bin/greet +1 -0
  29. data/evals/cases/chewie-verifier/verify-medium/pass/run.json +1 -0
  30. data/evals/cases/chewie-verifier/verify-medium.yml +48 -0
  31. data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/implementation-plan.md +5 -0
  32. data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/plan.md +3 -0
  33. data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/pull-requests.md +12 -0
  34. data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/spec.md +5 -0
  35. data/evals/cases/hansolo-reviewer/coverage-minimum/fail/repo/lib/cart.rb +10 -0
  36. data/evals/cases/hansolo-reviewer/coverage-minimum/fail/repo/spec/cart_spec.rb +9 -0
  37. data/evals/cases/hansolo-reviewer/coverage-minimum/fail/run.json +1 -0
  38. data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/implementation-plan.md +5 -0
  39. data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/plan.md +3 -0
  40. data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/pull-requests.md +15 -0
  41. data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/spec.md +5 -0
  42. data/evals/cases/hansolo-reviewer/coverage-minimum/pass/repo/lib/cart.rb +10 -0
  43. data/evals/cases/hansolo-reviewer/coverage-minimum/pass/repo/spec/cart_spec.rb +9 -0
  44. data/evals/cases/hansolo-reviewer/coverage-minimum/pass/run.json +1 -0
  45. data/evals/cases/hansolo-reviewer/coverage-minimum.yml +76 -0
  46. data/evals/cases/luthen-rael-planner/plan-deep/fail/plan/plan.md +8 -0
  47. data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan.md +2 -2
  48. data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep.yml +1 -1
  49. data/evals/cases/luthen-rael-planner/plan-fast/fail/plan/plan.md +12 -0
  50. data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan.md +2 -2
  51. data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast.yml +1 -1
  52. data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/plan.md +20 -0
  53. data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/pull-requests.md +5 -0
  54. data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/spec.md +5 -0
  55. data/evals/cases/palpatine-risk-assessor/risk-medium/fail/repo/db/migrate/20261020120000_drop_legacy_email.rb +5 -0
  56. data/evals/cases/palpatine-risk-assessor/risk-medium/fail/run.json +1 -0
  57. data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/plan.md +25 -0
  58. data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/pull-requests.md +5 -0
  59. data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/spec.md +5 -0
  60. data/evals/cases/palpatine-risk-assessor/risk-medium/pass/repo/db/migrate/20261020120000_drop_legacy_email.rb +5 -0
  61. data/evals/cases/palpatine-risk-assessor/risk-medium/pass/run.json +1 -0
  62. data/evals/cases/palpatine-risk-assessor/risk-medium.yml +47 -0
  63. data/evals/cases/yoda-writer/spec-medium/pass/plan/spec.md +1 -1
  64. data/lib/openlanes/adapters/base.rb +18 -4
  65. data/lib/openlanes/adapters/capabilities.rb +29 -0
  66. data/lib/openlanes/adapters/claude.rb +14 -11
  67. data/lib/openlanes/adapters/codex.rb +9 -0
  68. data/lib/openlanes/adapters/pi.rb +10 -0
  69. data/lib/openlanes/adapters/stream.rb +145 -1
  70. data/lib/openlanes/adapters.rb +7 -0
  71. data/lib/openlanes/agents/agent.rb +4 -3
  72. data/lib/openlanes/agents/roster.rb +14 -2
  73. data/lib/openlanes/agents/routing.rb +43 -3
  74. data/lib/openlanes/apps/definition.rb +152 -0
  75. data/lib/openlanes/apps/framing.rb +114 -0
  76. data/lib/openlanes/apps/listing.rb +60 -0
  77. data/lib/openlanes/apps/ports.rb +168 -0
  78. data/lib/openlanes/apps/probe.rb +63 -0
  79. data/lib/openlanes/apps/process_spec.rb +39 -0
  80. data/lib/openlanes/apps/runner.rb +174 -0
  81. data/lib/openlanes/apps/status.rb +62 -0
  82. data/lib/openlanes/apps/supervisor.rb +277 -0
  83. data/lib/openlanes/apps/watcher.rb +50 -0
  84. data/lib/openlanes/apps.rb +41 -0
  85. data/lib/openlanes/cli/api/api.rb +10 -0
  86. data/lib/openlanes/cli/api/subcommands/docs.rb +63 -0
  87. data/lib/openlanes/cli/app/app.rb +31 -0
  88. data/lib/openlanes/cli/app/subcommands/start.rb +114 -0
  89. data/lib/openlanes/cli/app/subcommands/status.rb +38 -0
  90. data/lib/openlanes/cli/app/subcommands/stop.rb +44 -0
  91. data/lib/openlanes/cli/base.rb +89 -16
  92. data/lib/openlanes/cli/coverage/coverage.rb +68 -0
  93. data/lib/openlanes/cli/create/create.rb +11 -265
  94. data/lib/openlanes/cli/eval/eval.rb +4 -1
  95. data/lib/openlanes/cli/init/init.rb +39 -2
  96. data/lib/openlanes/cli/linear/subcommands/import.rb +1 -1
  97. data/lib/openlanes/cli/linear/subcommands/projects.rb +1 -1
  98. data/lib/openlanes/cli/list_plans/list_plans.rb +8 -2
  99. data/lib/openlanes/cli/mail/subcommands/ack.rb +52 -0
  100. data/lib/openlanes/cli/mail/subcommands/render.rb +31 -0
  101. data/lib/openlanes/cli/mail/subcommands/send.rb +26 -0
  102. data/lib/openlanes/cli/resync/subcommands/dirs.rb +53 -12
  103. data/lib/openlanes/cli/resync/subcommands/prs.rb +8 -69
  104. data/lib/openlanes/cli/review/review.rb +72 -0
  105. data/lib/openlanes/cli/run/run.rb +22 -486
  106. data/lib/openlanes/cli/setup/setup.rb +51 -0
  107. data/lib/openlanes/cli/start/start.rb +7 -51
  108. data/lib/openlanes/cli/state/state.rb +1 -1
  109. data/lib/openlanes/cli/state/subcommands/resume.rb +61 -0
  110. data/lib/openlanes/cli/state/subcommands/sign.rb +1 -1
  111. data/lib/openlanes/cli/unblock/unblock.rb +11 -205
  112. data/lib/openlanes/cli/web/web.rb +84 -8
  113. data/lib/openlanes/cli/worktree/worktree.rb +1 -1
  114. data/lib/openlanes/cli/worktree/worktree_prune.rb +77 -0
  115. data/lib/openlanes/cli.rb +37 -2
  116. data/lib/openlanes/coverage/cobertura.rb +49 -0
  117. data/lib/openlanes/coverage/config.rb +194 -0
  118. data/lib/openlanes/coverage/istanbul.rb +31 -0
  119. data/lib/openlanes/coverage/lcov.rb +38 -0
  120. data/lib/openlanes/coverage/report.rb +71 -0
  121. data/lib/openlanes/coverage/simplecov.rb +70 -0
  122. data/lib/openlanes/coverage.rb +33 -0
  123. data/lib/openlanes/engine/agent_locks.rb +231 -0
  124. data/lib/openlanes/engine/arbitration.rb +25 -0
  125. data/lib/openlanes/engine/board.rb +36 -5
  126. data/lib/openlanes/engine/conversions.rb +160 -0
  127. data/lib/openlanes/engine/coverage_gate.rb +217 -0
  128. data/lib/openlanes/engine/dispatcher.rb +1175 -117
  129. data/lib/openlanes/engine/event_journal/replay.rb +96 -0
  130. data/lib/openlanes/engine/event_journal.rb +148 -0
  131. data/lib/openlanes/engine/extensions.rb +271 -0
  132. data/lib/openlanes/engine/merging.rb +459 -0
  133. data/lib/openlanes/engine/narrator.rb +6 -6
  134. data/lib/openlanes/engine/plan_locks.rb +114 -0
  135. data/lib/openlanes/engine/pr_outcomes.rb +161 -0
  136. data/lib/openlanes/engine/pr_watch.rb +83 -0
  137. data/lib/openlanes/engine/progress_log.rb +12 -120
  138. data/lib/openlanes/engine/quality_question.rb +125 -0
  139. data/lib/openlanes/engine/rebase.rb +200 -0
  140. data/lib/openlanes/engine/run_workflow.rb +776 -0
  141. data/lib/openlanes/engine/runner.rb +186 -15
  142. data/lib/openlanes/engine/start_workflow.rb +77 -0
  143. data/lib/openlanes/engine/state_file.rb +101 -34
  144. data/lib/openlanes/engine/tally.rb +1 -1
  145. data/lib/openlanes/engine/verification/app_session.rb +104 -0
  146. data/lib/openlanes/engine/verification/driver.rb +314 -0
  147. data/lib/openlanes/engine/verification/outcome.rb +516 -0
  148. data/lib/openlanes/engine/verification/report.rb +95 -0
  149. data/lib/openlanes/engine/verification/round.rb +100 -0
  150. data/lib/openlanes/engine/verification/screenshots.rb +120 -0
  151. data/lib/openlanes/engine/verification.rb +57 -0
  152. data/lib/openlanes/engine.rb +30 -1
  153. data/lib/openlanes/execution/boundary.rb +212 -0
  154. data/lib/openlanes/execution/child.rb +4 -2
  155. data/lib/openlanes/execution/clock.rb +36 -6
  156. data/lib/openlanes/execution/control.rb +32 -0
  157. data/lib/openlanes/execution/executor.rb +114 -35
  158. data/lib/openlanes/execution/process_start.rb +63 -0
  159. data/lib/openlanes/execution/prompt.rb +223 -34
  160. data/lib/openlanes/execution/transcript.rb +159 -8
  161. data/lib/openlanes/execution.rb +21 -0
  162. data/lib/openlanes/lifecycle/adoption.rb +25 -8
  163. data/lib/openlanes/lifecycle/aging.rb +166 -0
  164. data/lib/openlanes/lifecycle/allocator.rb +216 -0
  165. data/lib/openlanes/lifecycle/brief.rb +32 -2
  166. data/lib/openlanes/lifecycle/completion.rb +12 -6
  167. data/lib/openlanes/lifecycle/create_workflow.rb +272 -0
  168. data/lib/openlanes/lifecycle/creator.rb +17 -13
  169. data/lib/openlanes/lifecycle/lanes.rb +1 -1
  170. data/lib/openlanes/lifecycle/resync.rb +170 -42
  171. data/lib/openlanes/lifecycle/unblock_workflow.rb +234 -0
  172. data/lib/openlanes/lifecycle/unblocker.rb +76 -2
  173. data/lib/openlanes/lifecycle.rb +37 -1
  174. data/lib/openlanes/linear/mapping.rb +4 -1
  175. data/lib/openlanes/linear/unit.rb +1 -1
  176. data/lib/openlanes/live/chat.rb +60 -11
  177. data/lib/openlanes/live/choice_converter.rb +166 -0
  178. data/lib/openlanes/live/choice_judge.rb +109 -0
  179. data/lib/openlanes/live/claims.rb +9 -1
  180. data/lib/openlanes/live/command.rb +13 -10
  181. data/lib/openlanes/live/commands.rb +217 -30
  182. data/lib/openlanes/live/copilot/slash.rb +82 -0
  183. data/lib/openlanes/live/copilot.rb +38 -4
  184. data/lib/openlanes/live/event_bus/base.rb +52 -0
  185. data/lib/openlanes/live/event_bus/memory_bus.rb +14 -0
  186. data/lib/openlanes/live/event_bus/redis_bus.rb +10 -0
  187. data/lib/openlanes/live/event_bus/transaction.rb +27 -0
  188. data/lib/openlanes/live/event_bus.rb +29 -0
  189. data/lib/openlanes/live/interviewer.rb +146 -0
  190. data/lib/openlanes/live/keys.rb +56 -0
  191. data/lib/openlanes/live/mail.rb +143 -0
  192. data/lib/openlanes/live/memory_store.rb +12 -1
  193. data/lib/openlanes/live/news.rb +61 -0
  194. data/lib/openlanes/live/node.rb +20 -0
  195. data/lib/openlanes/live/outage.rb +225 -0
  196. data/lib/openlanes/live/outage_report.rb +89 -0
  197. data/lib/openlanes/live/publisher.rb +42 -2
  198. data/lib/openlanes/live/run_record.rb +39 -1
  199. data/lib/openlanes/live/run_state.rb +19 -2
  200. data/lib/openlanes/live/session.rb +31 -3
  201. data/lib/openlanes/live/snapshot.rb +103 -42
  202. data/lib/openlanes/live/stations.rb +4 -4
  203. data/lib/openlanes/live/store.rb +18 -1
  204. data/lib/openlanes/live/subscriber.rb +22 -2
  205. data/lib/openlanes/live.rb +66 -1
  206. data/lib/openlanes/plans/blocked_file.rb +117 -0
  207. data/lib/openlanes/plans/choices.rb +228 -0
  208. data/lib/openlanes/plans/dependencies.rb +304 -0
  209. data/lib/openlanes/plans/feature.rb +75 -16
  210. data/lib/openlanes/plans/journal.rb +169 -0
  211. data/lib/openlanes/plans/last_known_state.rb +82 -0
  212. data/lib/openlanes/plans/lock.rb +171 -0
  213. data/lib/openlanes/plans/mailbox.rb +145 -4
  214. data/lib/openlanes/plans/plan_state.rb +590 -9
  215. data/lib/openlanes/plans/pull_request_machine.rb +163 -0
  216. data/lib/openlanes/plans/review_label.rb +139 -0
  217. data/lib/openlanes/plans/spec.rb +239 -3
  218. data/lib/openlanes/plans/state_machine.rb +63 -30
  219. data/lib/openlanes/plans/status.rb +60 -11
  220. data/lib/openlanes/plans/subject.rb +4 -0
  221. data/lib/openlanes/plans/tree.rb +101 -10
  222. data/lib/openlanes/plans/verdict.rb +124 -0
  223. data/lib/openlanes/plans.rb +19 -0
  224. data/lib/openlanes/presentation/api_docs.rb +76 -0
  225. data/lib/openlanes/presentation/documentation.rb +105 -12
  226. data/lib/openlanes/presentation/pr_changes.rb +90 -0
  227. data/lib/openlanes/presentation/reporter.rb +64 -5
  228. data/lib/openlanes/presentation.rb +20 -1
  229. data/lib/openlanes/setup/check.rb +16 -0
  230. data/lib/openlanes/setup/machine.rb +179 -0
  231. data/lib/openlanes/setup.rb +34 -0
  232. data/lib/openlanes/support/clock.rb +57 -0
  233. data/lib/openlanes/support/config.rb +318 -6
  234. data/lib/openlanes/support/log.rb +206 -0
  235. data/lib/openlanes/support/refusal.rb +15 -0
  236. data/lib/openlanes/support/runtime.rb +61 -0
  237. data/lib/openlanes/support/spawner.rb +51 -0
  238. data/lib/openlanes/support/ui.rb +39 -24
  239. data/lib/openlanes/vcs/ci_gate.rb +31 -0
  240. data/lib/openlanes/vcs/github.rb +362 -12
  241. data/lib/openlanes/vcs/label_migration.rb +132 -0
  242. data/lib/openlanes/vcs/label_sync.rb +75 -0
  243. data/lib/openlanes/vcs/publisher.rb +226 -8
  244. data/lib/openlanes/vcs/refresh.rb +337 -0
  245. data/lib/openlanes/vcs/rename_date.rb +83 -0
  246. data/lib/openlanes/vcs/review.rb +66 -0
  247. data/lib/openlanes/vcs/worktree.rb +167 -6
  248. data/lib/openlanes/vcs.rb +18 -0
  249. data/lib/openlanes/version.rb +1 -1
  250. data/lib/openlanes/web/admin_auth.rb +104 -0
  251. data/lib/openlanes/web/app.rb +115 -16
  252. data/lib/openlanes/web/context.rb +15 -9
  253. data/lib/openlanes/web/demo.rb +29 -14
  254. data/lib/openlanes/web/demo_board.rb +63 -5
  255. data/lib/openlanes/web/events.rb +86 -7
  256. data/lib/openlanes/web/launcher.rb +69 -7
  257. data/lib/openlanes/web/listen.rb +133 -0
  258. data/lib/openlanes/web/plan_files.rb +136 -0
  259. data/lib/openlanes/web/request_log.rb +108 -0
  260. data/lib/openlanes/web/server.rb +29 -10
  261. data/lib/openlanes/web.rb +6 -4
  262. data/lib/openlanes.rb +45 -7
  263. data/openlanes.gemspec +9 -4
  264. data/schemas/journal-event.schema.json +72 -0
  265. data/schemas/plan-state.schema.json +163 -2
  266. data/skills/openlanes/SKILL.md +2 -2
  267. data/skills/openlanes-documents/SKILL.md +4 -4
  268. data/skills/openlanes-documents/references/blocked-template.md +2 -2
  269. data/skills/openlanes-documents/references/closing-templates.md +1 -1
  270. data/skills/openlanes-documents/references/plan-template.md +12 -4
  271. data/skills/openlanes-documents/references/retroactive-spec.md +1 -1
  272. data/skills/openlanes-run/SKILL.md +5 -5
  273. data/skills/openlanes-spec/SKILL.md +1 -1
  274. data/skills/openlanes-status/SKILL.md +1 -1
  275. data/web/dist/assets/{index-BFPI-zXI.js → index-690ayLS4.js} +2 -2
  276. data/web/dist/assets/index-6ZmW4slI.js +199 -0
  277. data/web/dist/assets/{index-bpsFIpXE.js → index-B5KzlXp1.js} +2 -2
  278. data/web/dist/assets/{index-DtA46i32.js → index-BAo_v9bU.js} +2 -2
  279. data/web/dist/assets/{index-CnJfB0l2.js → index-BDm7g-xG.js} +2 -2
  280. data/web/dist/assets/{index-DXgeajd6.js → index-BFH9bC7v.js} +2 -2
  281. data/web/dist/assets/{index-DH0gDlee.js → index-BVHPV7z4.js} +2 -2
  282. data/web/dist/assets/{index-DaSjQ-H4.js → index-B_CPqjnA.js} +2 -2
  283. data/web/dist/assets/{index-Dbxvk-EC.js → index-BhwBH5ct.js} +2 -2
  284. data/web/dist/assets/{index-6MZkmh3k.js → index-Bq5AdwEt.js} +2 -2
  285. data/web/dist/assets/{index-B5CKJPm-.js → index-CAW6ClM2.js} +2 -2
  286. data/web/dist/assets/index-Cp86u27u.css +1 -0
  287. data/web/dist/assets/{index-CnvkZu5Y.js → index-Culm4pMO.js} +2 -2
  288. data/web/dist/assets/{index-IqD8mBos.js → index-DElDJfp1.js} +2 -2
  289. data/web/dist/assets/{index-C3zc4gFe.js → index-Dja7osNW.js} +2 -2
  290. data/web/dist/assets/{index-C2jwoWVV.js → index-DjhU3FzU.js} +2 -2
  291. data/web/dist/assets/{index-pvJi-JJO.js → index-Dutbxj4o.js} +2 -2
  292. data/web/dist/assets/{index-BF6QYroq.js → index-RkDhPMDt.js} +2 -2
  293. data/web/dist/assets/{index-DbueqOK5.js → index-_mptZjlC.js} +2 -2
  294. data/web/dist/index.html +2 -2
  295. data/web/src/api/fixtures/board.json +203 -28
  296. metadata +1053 -889
  297. data/bin/factory-e2e +0 -157
  298. data/bin/plan-number +0 -164
  299. data/bin/setup +0 -26
  300. data/evals/cases/palpatine-planner/plan-deep/fail/plan/plan.md +0 -8
  301. data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan.md +0 -12
  302. data/web/dist/assets/index-BYJCWlk0.js +0 -198
  303. data/web/dist/assets/index-CjInBi5X.css +0 -1
  304. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/plan/plan-backend.md +0 -0
  305. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/plan/spec.md +0 -0
  306. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/run.json +0 -0
  307. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan-backend.md +0 -0
  308. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan-frontend.md +0 -0
  309. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/spec.md +0 -0
  310. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/run.json +0 -0
  311. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/plan-backend.md +0 -0
  312. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/plan-frontend.md +0 -0
  313. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/spec.md +0 -0
  314. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/run.json +0 -0
  315. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan-backend.md +0 -0
  316. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan-frontend.md +0 -0
  317. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/spec.md +0 -0
  318. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/run.json +0 -0
  319. /data/exe/{depot → ol} +0 -0
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ # Gets a machine ready to run the factory (plan 035): the tools in the
5
+ # gem's `Brewfile`, then what brew cannot install. `openlanes setup` and
6
+ # `openlanes init` both come through here, so the two ask the same
7
+ # questions of the machine and never disagree about whether it is ready.
8
+ module Setup
9
+ # The gem's own root, which an installed gem and a checkout both have.
10
+ GEM_ROOT = File.expand_path("../..", __dir__)
11
+
12
+ # The one Brewfile, shipped in the gem: the runtime tools and a
13
+ # contributor's, which `bin/setup` bundles from the same file.
14
+ BREWFILE = File.join(GEM_ROOT, "Brewfile")
15
+
16
+ module_function
17
+
18
+ # @return [OpenLanes::Setup::Machine]
19
+ def machine(**) = Machine.new(**)
20
+
21
+ # One line per check, ✓ or ✗, and under each ✗ the command that fixes it.
22
+ #
23
+ # @param checks [Array<OpenLanes::Setup::Check>]
24
+ # @return [String]
25
+ def render(checks)
26
+ width = checks.map { it.name.length }.max.to_i
27
+ checks.map do |check|
28
+ mark = check.ok? ? UI.paint("✓", :green) : UI.paint("✗", :red)
29
+ line = "#{mark} #{check.name.ljust(width)} #{check.detail}".rstrip
30
+ check.ok? || check.fix.nil? ? "#{line}\n" : "#{line}\n fix: #{check.fix}\n"
31
+ end.join
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ # A time source, so a spec can move time instead of waiting for it.
5
+ #
6
+ # Not to be confused with {OpenLanes::Execution::Clock}, the timeout timer
7
+ # with a thread of its own; inside `Execution` a bare `Clock` means that
8
+ # one, so code there writes `OpenLanes::Clock` in full.
9
+ module Clock
10
+ # The real clock.
11
+ module System
12
+ module_function
13
+
14
+ # @return [Time]
15
+ def now = Time.now
16
+
17
+ # @return [Float] seconds on a clock no wall-clock change can move
18
+ def monotonic = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
19
+
20
+ # @param seconds [Numeric]
21
+ # @return [void]
22
+ def sleep(seconds) = Kernel.sleep(seconds)
23
+ end
24
+
25
+ # A clock that only moves when told to. {#sleep} advances rather than
26
+ # blocks, so a loop that waits on it runs at once and still sees time pass.
27
+ # Shared between threads in the stress spec, hence the mutex.
28
+ class Fake
29
+ # @param now [Time]
30
+ def initialize(now: Time.utc(2026, 1, 1))
31
+ @now = now
32
+ @monotonic = 0.0
33
+ @mutex = Mutex.new
34
+ end
35
+
36
+ # @return [Time]
37
+ def now = @mutex.synchronize { @now }
38
+
39
+ # @return [Float]
40
+ def monotonic = @mutex.synchronize { @monotonic }
41
+
42
+ # @param seconds [Numeric]
43
+ # @return [self]
44
+ def advance(seconds)
45
+ @mutex.synchronize do
46
+ @now += seconds
47
+ @monotonic += seconds
48
+ end
49
+ self
50
+ end
51
+
52
+ # @param seconds [Numeric]
53
+ # @return [void]
54
+ def sleep(seconds) = advance(seconds)
55
+ end
56
+ end
57
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "date"
3
4
  require "yaml"
4
5
 
5
6
  module OpenLanes
@@ -10,9 +11,37 @@ module OpenLanes
10
11
  # frontend: false # a spec's `frontend:` when spec.md omits it
11
12
  # jobs: 4 # how many plans `openlanes run` works on at once
12
13
  # redis_url: redis://10.0.4.2:6379/0 # the Redis the live run shares (W16)
14
+ # redis_outage_hours: 1 # how long agents stay paused for a lost Redis
15
+ # # before the run shuts down (W32); any positive number
16
+ # redis_backoff: # how a lost Redis is asked again (plan 022)
17
+ # initial: 1 # seconds; the Fibonacci gaps are its multiples
18
+ # cap: 30 # seconds, the longest gap between two asks
19
+ # jitter: true # full jitter: each gap drawn from [0, gap)
20
+ # desired_codebase_quality: high # or mvp (R4): what a review that is ok but
21
+ # # not perfect does, and M5's column of defaults
22
+ # merge:
23
+ # by: human # who merges a reviewed PR (M4); `openlanes start` watches it
24
+ # max_rejections: 2 # rejections, hansolo's and chewie's together, before
25
+ # # the plan stops going back to its builders (M6)
13
26
  # commands: # per-command option defaults
14
27
  # run:
15
28
  # timeout: 1800
29
+ # questions:
30
+ # model: anthropic/claude-haiku-5.5 # rewrites prose questions into choices (D9)
31
+ # review:
32
+ # max_hmm: 5 # most 🤔 a review may carry and be approved (R3);
33
+ # # $OPENLANES_REVIEW_MAX_HMM beats it
34
+ # app: # how to run the app a plan builds (M12)
35
+ # kind: web
36
+ # processes:
37
+ # - name: web
38
+ # start: npx vite --port $PORT_WEB --strictPort
39
+ # ready: /
40
+ # preview: true
41
+ # coverage: # what the coverage gate measures (M15)
42
+ # command: bundle exec rspec
43
+ # report_command: jq '.total.lines.percent' coverage/coverage.json
44
+ # minimum: 95 # or report: + format: instead of report_command:
16
45
  #
17
46
  # Precedence, loudest first: a flag typed on the command line, the plan's
18
47
  # spec.md frontmatter, this file, the built-in default. The file supplies
@@ -40,14 +69,111 @@ module OpenLanes
40
69
  # @!attribute [r] redis_url
41
70
  # @return [String, nil] where the live run keeps its board, its commands
42
71
  # and its plan claims; `--redis-url` beats it, and it beats `$REDIS_URL`
72
+ # @!attribute [r] redis_outage_hours
73
+ # @return [Numeric, nil] `redis_outage_hours` as the file says it; read
74
+ # {#outage_limit}, which applies the default
75
+ # @!attribute [r] redis_backoff
76
+ # @return [Hash{Symbol => Object}] `redis_backoff:` as typed, keyed
77
+ # :initial, :cap, :jitter; {Live::Outage} applies the defaults
43
78
  # @!attribute [r] commands
44
79
  # @return [Hash{String => Hash{Symbol => Object}}] by command name, as
45
80
  # typed: "run", "resync dirs"
46
- Config = Data.define(:path, :project, :lane, :frontend, :jobs, :redis_url, :commands) do
47
- def initialize(path: nil, project: nil, lane: nil, frontend: nil, jobs: nil, redis_url: nil, commands: {})
81
+ # @!attribute [r] merge_by
82
+ # @return [Symbol, nil] `merge.by`, one of {MERGERS}; nil reads as {#merger}'s default
83
+ # @!attribute [r] merge_max_rejections
84
+ # @return [Integer, nil] `merge.max_rejections` as the file says it; read
85
+ # {#max_rejections}, which applies the default
86
+ # @!attribute [r] app
87
+ # @return [OpenLanes::Apps::Definition, nil] the processes `openlanes app
88
+ # start` runs; nil when the project declares none, and then nothing
89
+ # about apps happens anywhere
90
+ # @!attribute [r] aging_since
91
+ # @return [Date, nil] `plans.aging_since`: no blocked plan ages from
92
+ # before it, so weeks the project sat paused do not count; nil is no floor
93
+ # @!attribute [r] questions_model
94
+ # @return [String, nil] `questions.model`: the OpenRouter model that
95
+ # rewrites a prose question into lettered choices; nil takes
96
+ # {Live::ChoiceConverter::DEFAULT_MODEL}
97
+ # @!attribute [r] review_max_hmm
98
+ # @return [Integer, nil] `review.max_hmm` as the file says it; read
99
+ # {#max_hmm}, which applies the environment and the default
100
+ # @!attribute [r] desired_codebase_quality
101
+ # @return [Symbol, nil] one of {QUALITIES}, as the file says it; nil
102
+ # while the project is still to be asked (R4)
103
+ # @!attribute [r] coverage
104
+ # @return [OpenLanes::Coverage::Config, nil] the `coverage:` section; nil
105
+ # when the project declares none, and then the gate sends the pull
106
+ # request back to its builder to write one
107
+ Config = Data.define(:path,
108
+ :project,
109
+ :lane,
110
+ :frontend,
111
+ :jobs,
112
+ :redis_url,
113
+ :redis_outage_hours,
114
+ :redis_backoff,
115
+ :commands,
116
+ :merge_by,
117
+ :app,
118
+ :aging_since,
119
+ :questions_model,
120
+ :review_max_hmm,
121
+ :merge_max_rejections,
122
+ :desired_codebase_quality,
123
+ :coverage) do
124
+ def initialize(path: nil, project: nil, lane: nil, frontend: nil, jobs: nil, redis_url: nil,
125
+ redis_outage_hours: nil, redis_backoff: {}, commands: {}, merge_by: nil, app: nil, aging_since: nil, questions_model: nil, review_max_hmm: nil, merge_max_rejections: nil,
126
+ desired_codebase_quality: nil, coverage: nil)
48
127
  super
49
128
  end
50
129
 
130
+ # How many times a plan's pull request may be rejected, by hansolo and
131
+ # chewie counted together, before it stops going back to its builders
132
+ # (M6). `merge.max_rejections` when the file says it, otherwise M5's
133
+ # column for `desired_codebase_quality:` (R4); two, `high`'s, when neither is set.
134
+ #
135
+ # @return [Integer]
136
+ def max_rejections
137
+ merge_max_rejections || Config::MAX_REJECTIONS_BY_QUALITY.fetch(desired_codebase_quality, Config::DEFAULT_MAX_REJECTIONS)
138
+ end
139
+
140
+ # @return [Boolean] whether the file has no `desired_codebase_quality:`,
141
+ # and so the person is still to be asked (R4)
142
+ def quality_unset? = desired_codebase_quality.nil?
143
+
144
+ # Seconds a run keeps its agents paused for a Redis that stopped
145
+ # answering before it gives up and shuts down (W32): an hour, unless
146
+ # `redis_outage_hours` says otherwise.
147
+ #
148
+ # @return [Float]
149
+ def outage_limit = (redis_outage_hours || Config::DEFAULT_OUTAGE_HOURS) * 3600.0
150
+
151
+ # The most 🤔 a review may carry and still be `review-approved` (R3).
152
+ # Loudest first: `$OPENLANES_REVIEW_MAX_HMM`, then `review.max_hmm`, then
153
+ # {Plans::Verdict::DEFAULT_MAX_HMM}. The variable is there so one run can
154
+ # be stricter or looser without editing a file the whole team shares.
155
+ #
156
+ # @param env [Hash] the environment, injectable for tests
157
+ # @return [Integer]
158
+ # @raise [OpenLanes::Error] when the variable is not a whole number
159
+ def max_hmm(env: ENV)
160
+ raw = env[Config::MAX_HMM_ENV].to_s.strip
161
+ return review_max_hmm || Plans::Verdict::DEFAULT_MAX_HMM if raw.empty?
162
+ return Integer(raw, 10) if raw.match?(/\A\d+\z/)
163
+
164
+ raise Error, "#{Config::MAX_HMM_ENV}=#{raw.inspect} is not a whole number"
165
+ end
166
+
167
+ # Who merges a pull request once its review passed (M4). A person, unless
168
+ # the file says otherwise, since merging is the one thing no agent may do.
169
+ #
170
+ # @return [Symbol] `:human` or `:auto`
171
+ def merger = merge_by || Config::MERGERS.first
172
+
173
+ # @return [Boolean] whether the engine waits on a person to merge, and so
174
+ # watches the pull requests it opened
175
+ def human_merges? = merger == :human
176
+
51
177
  # One command's option defaults, the top-level spelling folded in.
52
178
  #
53
179
  # @param command [String, Symbol] e.g. :run or "resync dirs"
@@ -70,10 +196,25 @@ module OpenLanes
70
196
  # @return [String]
71
197
  def to_yaml
72
198
  settings = { "project" => project, "lane" => lane&.to_s, "frontend" => frontend, "jobs" => jobs,
73
- "redis_url" => redis_url }
199
+ "redis_url" => redis_url, "redis_outage_hours" => redis_outage_hours,
200
+ "redis_backoff" => (redis_backoff.transform_keys(&:to_s) unless redis_backoff.empty?),
201
+ "desired_codebase_quality" => desired_codebase_quality&.to_s, "merge" => merge_settings }
74
202
  settings["commands"] = commands.transform_values { it.transform_keys(&:to_s) } unless commands.empty?
203
+ settings["app"] = app.to_h if app
204
+ settings["plans"] = { "aging_since" => aging_since.iso8601 } if aging_since
205
+ settings["questions"] = { "model" => questions_model } if questions_model
206
+ settings["review"] = { "max_hmm" => review_max_hmm } if review_max_hmm
207
+ settings["coverage"] = coverage.to_h if coverage
75
208
  YAML.dump(settings.compact).delete_prefix("---\n")
76
209
  end
210
+
211
+ private
212
+
213
+ # @return [Hash, nil] `merge:` as the file would hold it, nil when nothing is set
214
+ def merge_settings
215
+ merge = { "by" => merge_by&.to_s, "max_rejections" => merge_max_rejections }.compact
216
+ merge.empty? ? nil : merge
217
+ end
77
218
  end
78
219
 
79
220
  class Config
@@ -81,7 +222,42 @@ module OpenLanes
81
222
  FILE = Project::FILE
82
223
 
83
224
  # Every key the top level may hold.
84
- KEYS = %w[project lane frontend jobs redis_url commands].freeze
225
+ KEYS = %w[project lane frontend jobs redis_url redis_outage_hours redis_backoff desired_codebase_quality merge commands app plans
226
+ questions review coverage].freeze
227
+
228
+ # What `desired_codebase_quality:` may say (R4), the default first. M5's
229
+ # `top_notch` and `medium` were dropped.
230
+ QUALITIES = %i[high mvp].freeze
231
+
232
+ # Who may merge, the default first.
233
+ MERGERS = %i[human auto].freeze
234
+
235
+ # Every key `merge:` may hold.
236
+ MERGE_KEYS = %w[by max_rejections].freeze
237
+
238
+ # {#max_rejections} when the file names none (M6).
239
+ DEFAULT_MAX_REJECTIONS = 2
240
+
241
+ # M5's `max_rejections` row, for the two columns R4 kept.
242
+ MAX_REJECTIONS_BY_QUALITY = { high: 2, mvp: 1 }.freeze
243
+
244
+ # {#outage_limit} when the file names none (W32).
245
+ DEFAULT_OUTAGE_HOURS = 1
246
+
247
+ # Every key `redis_backoff:` may hold.
248
+ BACKOFF_KEYS = %w[initial cap jitter].freeze
249
+
250
+ # Every key `plans:` may hold. Checked key by key, like the top level.
251
+ PLAN_KEYS = %w[aging_since].freeze
252
+
253
+ # Every key `questions:` may hold.
254
+ QUESTION_KEYS = %w[model].freeze
255
+
256
+ # Every key `review:` may hold.
257
+ REVIEW_KEYS = %w[max_hmm].freeze
258
+
259
+ # The variable that beats `review.max_hmm`.
260
+ MAX_HMM_ENV = "OPENLANES_REVIEW_MAX_HMM"
85
261
 
86
262
  # Schemes a `redis_url:` may use.
87
263
  REDIS_SCHEMES = %w[redis rediss unix].freeze
@@ -118,7 +294,7 @@ module OpenLanes
118
294
  # @param path [String] for every error message
119
295
  # @return [OpenLanes::Config]
120
296
  def parse(text, path:)
121
- data = YAML.safe_load(text) || {}
297
+ data = YAML.safe_load(text, permitted_classes: [Date]) || {}
122
298
  fail_with(path, "expected a mapping of settings at the top") unless data.is_a?(Hash)
123
299
 
124
300
  settings = data.transform_keys(&:to_s)
@@ -135,7 +311,16 @@ module OpenLanes
135
311
  frontend: frontend(settings, path),
136
312
  jobs: jobs(settings, path),
137
313
  redis_url: redis_url(settings, path),
138
- commands: commands(settings["commands"], path))
314
+ redis_outage_hours: redis_outage_hours(settings, path),
315
+ redis_backoff: redis_backoff(settings["redis_backoff"], path),
316
+ desired_codebase_quality: quality(settings, path),
317
+ **merge(settings["merge"], path),
318
+ commands: commands(settings["commands"], path),
319
+ app: settings.key?("app") ? Apps::Definition.parse(settings["app"], path:) : nil,
320
+ **plans(settings["plans"], path),
321
+ **questions(settings["questions"], path),
322
+ **review(settings["review"], path),
323
+ coverage: settings.key?("coverage") ? Coverage::Config.parse(settings["coverage"], path:) : nil)
139
324
  conflicts!(config, path)
140
325
  config
141
326
  rescue Psych::Exception => e
@@ -191,6 +376,74 @@ module OpenLanes
191
376
  fail_with(path, "redis_url: #{value.inspect} is not a #{REDIS_SCHEMES.map { "#{it}://" }.join(", ")} URL")
192
377
  end
193
378
 
379
+ # Any positive number: half an hour is `0.5`.
380
+ #
381
+ # @return [Numeric, nil]
382
+ def redis_outage_hours(settings, path)
383
+ value = settings["redis_outage_hours"]
384
+ return value if value.nil? || (value.is_a?(Numeric) && value.positive?)
385
+
386
+ fail_with(path, "redis_outage_hours: #{value.inspect} is not a number of hours above zero")
387
+ end
388
+
389
+ # @return [Hash{Symbol => Object}] `redis_backoff:`, checked key by key
390
+ def redis_backoff(value, path)
391
+ return {} if value.nil?
392
+
393
+ fail_with(path, "redis_backoff: must be a mapping of setting to value") unless value.is_a?(Hash)
394
+
395
+ value.transform_keys(&:to_s).to_h do |key, setting|
396
+ fail_with(path, "redis_backoff.#{key}: unknown key; known: #{BACKOFF_KEYS.join(", ")}") unless BACKOFF_KEYS.include?(key)
397
+ if key == "jitter"
398
+ fail_with(path, "redis_backoff.jitter: #{setting.inspect} is not true or false") unless [true, false].include?(setting)
399
+ elsif !(setting.is_a?(Numeric) && setting.positive?)
400
+ fail_with(path, "redis_backoff.#{key}: #{setting.inspect} is not a number of seconds above zero")
401
+ end
402
+ [key.to_sym, setting]
403
+ end
404
+ end
405
+
406
+ # @return [Symbol, nil]
407
+ def quality(settings, path)
408
+ return nil unless settings.key?("desired_codebase_quality")
409
+
410
+ value = settings["desired_codebase_quality"].to_s.strip.downcase.to_sym
411
+ return value if QUALITIES.include?(value)
412
+
413
+ fail_with(path, "desired_codebase_quality: #{settings["desired_codebase_quality"].inspect} is not one of #{QUALITIES.join(", ")}")
414
+ end
415
+
416
+ # @return [Hash{Symbol => Object}] the `merge:` settings, as keywords for {Config.new}
417
+ def merge(value, path)
418
+ return {} if value.nil?
419
+
420
+ fail_with(path, "merge: must be a mapping, e.g. `merge: { by: human }`") unless value.is_a?(Hash)
421
+
422
+ settings = value.transform_keys(&:to_s)
423
+ unknown = settings.keys - MERGE_KEYS
424
+ fail_with(path, "merge: unknown key #{unknown.join(", ")}; known: #{MERGE_KEYS.join(", ")}") unless unknown.empty?
425
+
426
+ { merge_by: merge_by(settings, path), merge_max_rejections: max_rejections(settings, path) }.compact
427
+ end
428
+
429
+ # @return [Symbol, nil]
430
+ def merge_by(settings, path)
431
+ return nil unless settings.key?("by")
432
+
433
+ by = settings["by"].to_s.strip.downcase.to_sym
434
+ return by if MERGERS.include?(by)
435
+
436
+ fail_with(path, "merge.by: #{settings["by"].inspect} is not one of #{MERGERS.join(", ")}")
437
+ end
438
+
439
+ # @return [Integer, nil]
440
+ def max_rejections(settings, path)
441
+ max = settings["max_rejections"]
442
+ return max if max.nil? || (max.is_a?(Integer) && max.positive?)
443
+
444
+ fail_with(path, "merge.max_rejections: #{max.inspect} is not a whole number above zero")
445
+ end
446
+
194
447
  # @return [Hash{String => Hash{Symbol => Object}}]
195
448
  def commands(value, path)
196
449
  return {} if value.nil?
@@ -204,6 +457,65 @@ module OpenLanes
204
457
  end
205
458
  end
206
459
 
460
+ # @return [Hash{Symbol => Object}] the `plans:` settings, as keywords for {Config.new}
461
+ def plans(value, path)
462
+ return {} if value.nil?
463
+
464
+ fail_with(path, "plans: must be a mapping of setting to value") unless value.is_a?(Hash)
465
+
466
+ settings = value.transform_keys(&:to_s)
467
+ (settings.keys - PLAN_KEYS).each do |key|
468
+ fail_with(path, "plans.#{key}: unknown key; known: #{PLAN_KEYS.join(", ")}")
469
+ end
470
+ { aging_since: aging_since(settings["aging_since"], path) }
471
+ end
472
+
473
+ # @return [Hash{Symbol => Object}] the `questions:` settings, as keywords for {Config.new}
474
+ def questions(value, path)
475
+ return {} if value.nil?
476
+
477
+ fail_with(path, "questions: must be a mapping of setting to value") unless value.is_a?(Hash)
478
+
479
+ settings = value.transform_keys(&:to_s)
480
+ (settings.keys - QUESTION_KEYS).each do |key|
481
+ fail_with(path, "questions.#{key}: unknown key; known: #{QUESTION_KEYS.join(", ")}")
482
+ end
483
+ model = settings["model"]
484
+ return {} if model.nil?
485
+ return { questions_model: model.strip } if model.is_a?(String) && !model.strip.empty?
486
+
487
+ fail_with(path, "questions.model: #{model.inspect} is not a model id, e.g. #{Live::ChoiceConverter::DEFAULT_MODEL}")
488
+ end
489
+
490
+ # @return [Hash{Symbol => Object}] the `review:` settings, as keywords for {Config.new}
491
+ def review(value, path)
492
+ return {} if value.nil?
493
+
494
+ fail_with(path, "review: must be a mapping of setting to value") unless value.is_a?(Hash)
495
+
496
+ settings = value.transform_keys(&:to_s)
497
+ (settings.keys - REVIEW_KEYS).each do |key|
498
+ fail_with(path, "review.#{key}: unknown key; known: #{REVIEW_KEYS.join(", ")}")
499
+ end
500
+ max = settings["max_hmm"]
501
+ return {} if max.nil?
502
+ return { review_max_hmm: max } if max.is_a?(Integer) && !max.negative?
503
+
504
+ fail_with(path, "review.max_hmm: #{max.inspect} is not a whole number")
505
+ end
506
+
507
+ # YAML hands an unquoted `2026-10-01` over as a Date already, and a
508
+ # quoted one as a String; both are the same day to whoever typed it.
509
+ #
510
+ # @return [Date, nil]
511
+ def aging_since(value, path)
512
+ return value if value.nil? || value.instance_of?(Date)
513
+
514
+ Date.iso8601(value.to_s)
515
+ rescue Date::Error
516
+ fail_with(path, "plans.aging_since: #{value.inspect} is not a date (YYYY-MM-DD)")
517
+ end
518
+
207
519
  # @return [void]
208
520
  def conflicts!(config, path)
209
521
  SHORTHANDS.each do |key, owner|