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,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module Engine
5
+ module Verification
6
+ # The plan's app, up for exactly as long as chewie runs (M1, M12):
7
+ # started in the plan's checkout through {Apps.supervisor}, and stopped
8
+ # in `ensure`, whatever the agent did, so no verification leaves a dev
9
+ # server holding a port from the shared registry.
10
+ #
11
+ # A project with no `app:` starts nothing and chewie verifies through the
12
+ # shell (M11), which is how openlanes itself, a CLI gem, is verified.
13
+ class AppSession
14
+ # The app did not come up, so chewie is never started: a verifier
15
+ # pointed at a dead port can only report what the engine already
16
+ # knows. The rejection quotes {#tail} (M1: the last 20 lines).
17
+ #
18
+ # @!attribute [r] process
19
+ # @return [String, nil] the process that failed
20
+ # @!attribute [r] reason
21
+ # @return [String]
22
+ # @!attribute [r] tail
23
+ # @return [Array<String>] that process's last lines
24
+ Failed = Data.define(:process, :reason, :tail) do
25
+ # @return [String] the opening of every rejection it causes
26
+ def headline = "the app does not start"
27
+
28
+ # @return [String] Markdown, for the report and the comment
29
+ def to_markdown
30
+ first = reason.to_s.lines.first.to_s.strip
31
+ lines = ["#{headline}: #{first}"]
32
+ unless tail.empty?
33
+ lines << "" << "The last #{tail.size} lines #{process || "it"} printed:" << "" << "```text"
34
+ lines.concat(tail) << "```"
35
+ end
36
+ lines.join("\n")
37
+ end
38
+ end
39
+
40
+ # @param definition [OpenLanes::Apps::Definition, nil] nil when the
41
+ # project declares no app
42
+ # @param root [String] the plan's checkout
43
+ # @param plan [String] the ordinal
44
+ # @param branch [String]
45
+ # @param supervisor_factory [#call] keywords to a supervisor; the
46
+ # suite passes one with its own port registry
47
+ def initialize(definition:, root:, plan:, branch:, supervisor_factory: Apps.method(:supervisor))
48
+ @definition = definition
49
+ @root = root
50
+ @plan = plan
51
+ @branch = branch
52
+ @factory = supervisor_factory
53
+ end
54
+
55
+ # @yieldparam statuses [Array<OpenLanes::Apps::Status>] every process,
56
+ # ready; empty when there is no app
57
+ # @return [Object, Failed] the block's value, or {Failed} without
58
+ # calling the block
59
+ def around
60
+ return yield([]) if @definition.nil?
61
+
62
+ supervisor = @factory.call(definition: @definition, root: @root, plan: @plan, branch: @branch)
63
+ begin
64
+ supervisor.start
65
+ rescue Apps::Supervisor::StartFailed => e
66
+ return Failed.new(process: e.process, reason: e.message, tail: e.process ? supervisor.log_tail(e.process) : [])
67
+ end
68
+ yield supervisor.statuses
69
+ ensure
70
+ supervisor&.stop
71
+ end
72
+
73
+ # What chewie is told about the app, written into its round's folder
74
+ # before it starts, so the ports reach it without the prompt (which
75
+ # is the executor's) having to know apps exist.
76
+ #
77
+ # @param statuses [Array<OpenLanes::Apps::Status>]
78
+ # @param definition [OpenLanes::Apps::Definition, nil]
79
+ # @return [String] Markdown
80
+ def self.note(statuses, definition)
81
+ return <<~MARKDOWN if statuses.empty?
82
+ # The app
83
+
84
+ This project declares no `app:` in `.openlanes.yml`, so nothing was started.
85
+ Follow `## How to test` through the shell (M11).
86
+ MARKDOWN
87
+
88
+ envs = definition.processes.to_h { [it.name, it.port_env] }
89
+ rows = statuses.map { "| #{it.process} | `#{envs[it.process]}=#{it.port}` | #{it.url} | #{"yes" if it.preview} |" }
90
+ <<~MARKDOWN
91
+ # The app
92
+
93
+ The engine started every process below, ready, and stops them when you finish.
94
+ Do not start or stop them yourself.
95
+
96
+ | Process | Port | URL | Preview |
97
+ | :------ | :--- | :-- | :------ |
98
+ #{rows.join("\n")}
99
+ MARKDOWN
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,314 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module Engine
5
+ module Verification
6
+ # Everything the dispatcher asks about verification, in one object, so
7
+ # the loop gains a few calls rather than a second job: the hand-off
8
+ # from hansolo, the app around chewie's run, the verdict, and the CI
9
+ # wait after a pass.
10
+ #
11
+ # The wait is read from disk, not remembered: a 🧪 plan whose newest
12
+ # round says `verified` is waiting on CI. A run that ends while it
13
+ # waits leaves nothing behind to clean up, and the next run, or a
14
+ # staying one, picks the wait up where it stood.
15
+ class Driver
16
+ # One plan's wait, settled.
17
+ #
18
+ # @!attribute [r] plan
19
+ # @return [String] the ordinal
20
+ # @!attribute [r] result
21
+ # @return [Outcome::Result]
22
+ # @!attribute [r] round
23
+ # @return [Integer, nil] the round it settled
24
+ Settled = Data.define(:plan, :result, :round) do
25
+ def initialize(round: nil, **rest) = super
26
+ end
27
+
28
+ # Seconds between looks at CI, as {PrWatch} paces its own.
29
+ INTERVAL = 15
30
+
31
+ # @param outcome [Outcome]
32
+ # @param worktree [OpenLanes::Vcs::Worktree] where a waiting plan's checkout is
33
+ # @param app [OpenLanes::Apps::Definition, nil] the project's `app:`
34
+ # @param supervisor_factory [#call] see {AppSession#initialize}
35
+ # @param clock [#call] monotonic seconds
36
+ # @param interval [Numeric] seconds between looks at CI
37
+ # @param quality [Symbol, nil, #call] {Config#desired_codebase_quality},
38
+ # nil while the project is still to be asked (R4); a callable is
39
+ # asked each time, so an answer given mid-run counts at once
40
+ def initialize(outcome:, worktree:, app: nil, supervisor_factory: Apps.method(:supervisor),
41
+ clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }, interval: INTERVAL, quality: nil)
42
+ @outcome = outcome
43
+ @quality = quality
44
+ @worktree = worktree
45
+ @app = app
46
+ @factory = supervisor_factory
47
+ @clock = clock
48
+ @interval = interval
49
+ @last = nil
50
+ @thread = nil
51
+ @ready = Set.new
52
+ @looking = []
53
+ end
54
+
55
+ # @return [Outcome]
56
+ attr_reader :outcome
57
+
58
+ # hansolo approved: on to 🧪, when the pull request carries a verdict
59
+ # that goes on to verification ({Verification.from_review?}) and is
60
+ # built from the branch chewie would be run in. A
61
+ # `review-ok-not-perfect` one goes back to 🔴 under `high` (R4), and
62
+ # stays 👀 while the project has not said which quality it wants. The checkout's branch
63
+ # is derived from the plan, and an adopted pull request may be built
64
+ # from any other; verifying it there would test the wrong code.
65
+ #
66
+ # @param subject [OpenLanes::Plans::Subject] read fresh
67
+ # @return [String, nil] what happened, for the attempt; nil when the
68
+ # folder cannot be 🧪 at all, and the plan finishes at 👀 as before
69
+ def hand_off(subject)
70
+ return nil unless subject.machine.may?(:verifying)
71
+
72
+ pull = open_pull(subject) or return nil
73
+ labels = @outcome.labels(pull)
74
+ sent = quality_says(subject, pull, labels)
75
+ return sent if sent
76
+
77
+ unless Verification.from_review?(labels, quality:)
78
+ verdict = (labels & Plans::ReviewLabel.names).join(", ")
79
+ return "stays 👀: PR ##{pull.number.to_s.delete("#")} is labelled #{verdict.empty? ? "nothing" : verdict}, " \
80
+ "and only #{FROM_REVIEW.map(&:name).join(", ")} goes on to verification"
81
+ end
82
+
83
+ mismatch = @outcome.mismatch(checkout: @worktree.checkout_for(subject.feature), pull:)
84
+ return "stays 👀: #{mismatch}; verify it by hand" if mismatch
85
+
86
+ # A round of its own for this hand-off, before the rename carries
87
+ # it into 🧪: the newest round otherwise may be one verified before
88
+ # a rejection and a rebuild, and would be taken for this one,
89
+ # waiting on CI, with the fixes never verified.
90
+ Verification.round_for(subject.feature.path).make!
91
+ subject.machine.promote!(:verifying)
92
+ "on to 🧪 verification"
93
+ rescue OpenLanes::Error => e
94
+ "stays 👀: #{e.message}"
95
+ end
96
+
97
+ # The app up around chewie's run, and chewie told where it is.
98
+ #
99
+ # @param task [OpenLanes::Engine::Runner::Task]
100
+ # @param subject [OpenLanes::Plans::Subject]
101
+ # @yieldreturn [Object] what the executor returned
102
+ # @return [Object, AppSession::Failed]
103
+ def around(task, subject)
104
+ round = Verification.round_for(subject.feature.path).make!
105
+ session = AppSession.new(definition: @app,
106
+ root: task.root,
107
+ plan: subject.feature.ordinal.to_s,
108
+ branch: task.checkout&.branch.to_s,
109
+ supervisor_factory: @factory)
110
+ session.around do |statuses|
111
+ File.write(File.join(round.dir, Round::APP_NOTE), AppSession.note(statuses, @app))
112
+ yield
113
+ end
114
+ end
115
+
116
+ # @param task [OpenLanes::Engine::Runner::Task]
117
+ # @param subject [OpenLanes::Plans::Subject] read fresh
118
+ # @param why [String, nil] see {Outcome#reject}
119
+ # @return [Outcome::Result, nil] nil when the plan has no open pull request to reject
120
+ def reject(task, subject, why: nil)
121
+ pull = open_pull(subject) or return nil
122
+
123
+ @outcome.reject(checkout: checkout_of(task, subject), plan: subject, pull:, round: latest(subject), why:)
124
+ end
125
+
126
+ # @param task [OpenLanes::Engine::Runner::Task]
127
+ # @param subject [OpenLanes::Plans::Subject] read fresh
128
+ # @return [Outcome::Result, nil] nil when the plan has no open pull request
129
+ def pass(task, subject)
130
+ pull = open_pull(subject) or return nil
131
+
132
+ round = latest(subject)
133
+ result = @outcome.pass(checkout: checkout_of(task, subject), plan: subject, pull:, round:)
134
+ # Only now is the round waiting on CI ({#awaiting_ci?}).
135
+ round.publish!(result.sha) if result.kind == :committed
136
+ result
137
+ end
138
+
139
+ # @param subject [OpenLanes::Plans::Subject]
140
+ # A verified round counts only once the engine pushed its evidence
141
+ # ({Round#published?}); one chewie reported but the engine never
142
+ # published goes back to chewie, in a new round.
143
+ #
144
+ # @return [Boolean] whether the plan waits on CI rather than on chewie
145
+ def awaiting_ci?(subject)
146
+ return false unless subject.status.key == :verifying
147
+
148
+ round = Round.latest(subject.feature.path)
149
+ !round.nil? && round.published? && round.report&.verdict == :verified
150
+ end
151
+
152
+ # Look at CI for every plan waiting on it, on a thread of its own:
153
+ # the coverage gate runs the project's whole suite, and the tick may
154
+ # not wait for that. The thread only reads ({Outcome#look}). What it
155
+ # found is acted on here, on the dispatcher's thread, on the first
156
+ # call after it finished: labelling, rejecting, mailing and renaming
157
+ # are lifecycle changes, and doing them on the thread raced the PR
158
+ # watcher and the builders over the same folder.
159
+ #
160
+ # Each plan is checked again before its look is acted on, since the
161
+ # look may have taken minutes: still 🧪, still waiting on the same
162
+ # round, still the same open pull request. One that changed is left
163
+ # for the next look.
164
+ #
165
+ # @param subjects [Array<OpenLanes::Plans::Subject>] plans the loop
166
+ # may touch, read fresh, the ones being looked at included
167
+ # @return [Array<Settled>] finished looks, often none
168
+ def poll(subjects)
169
+ if @thread && !@thread.alive?
170
+ looked = @thread.value || []
171
+ @thread = nil
172
+ @looking = []
173
+ return looked.filter_map { |pending, seen| apply(pending, seen, subjects) }
174
+ end
175
+ return [] if @thread || !due?
176
+
177
+ waiting = subjects.select { awaiting_ci?(it) && !@ready.include?(ready_key(it)) }.filter_map { pending_for(it) }
178
+ return [] if waiting.empty?
179
+
180
+ @looking = waiting.map(&:plan)
181
+ @thread = Thread.new { waiting.filter_map { look_at(it) } }
182
+ []
183
+ end
184
+
185
+ # @return [Boolean] whether a look is under way, or finished and not
186
+ # yet acted on: a run may not end before {#poll} has had it
187
+ def busy? = !@thread.nil?
188
+
189
+ # @return [Array<String>] ordinals being looked at: no agent may start
190
+ # on them and no rename may touch them until the look is acted on
191
+ def looking = @looking.dup
192
+
193
+ # @return [void] waits for a look under way
194
+ def join = @thread&.join
195
+
196
+ private
197
+
198
+ # What R4 does with a `review-ok-not-perfect` pull request, short of
199
+ # verifying it.
200
+ #
201
+ # @return [String, nil] what happened; nil when the quality lets it
202
+ # go on, or the verdict is not the one the quality decides
203
+ def quality_says(subject, pull, labels)
204
+ now = quality
205
+ return nil unless Verification.ok_not_perfect?(labels) && now != :mvp
206
+
207
+ number = pull.number.to_s.delete("#")
208
+ return unasked(subject, number) if now.nil?
209
+
210
+ return nil unless subject.machine.may?(:rejected)
211
+
212
+ # Counted, as any rejection is, so a PR labelled the same round
213
+ # after round reaches `merge.max_rejections` and the arbitrator.
214
+ result = @outcome.send_back_for_quality(plan: subject, pull:)
215
+ "back to 🔴: #{result.note}; luke and rey answer every comment that is not ✅"
216
+ end
217
+
218
+ # @return [Symbol, nil]
219
+ def quality = @quality.respond_to?(:call) ? @quality.call : @quality
220
+
221
+ # The key is missing: R4's question goes to a person, once per
222
+ # project, and the plan waits on it at 👀.
223
+ #
224
+ # @return [String]
225
+ def unasked(subject, number)
226
+ asked = QualityQuestion.ask!(subject.feature.path) ? "asked in the drawer" : "already asked on another plan"
227
+ "stays 👀: PR ##{number} is labelled #{Verification::ON_QUALITY.name}, " \
228
+ "and desired_codebase_quality is not set in #{Config::FILE}; say high or mvp (#{asked})"
229
+ end
230
+
231
+ # One plan's look, as it stood when the look began.
232
+ Pending = Data.define(:plan, :subject, :round, :pull, :checkout)
233
+
234
+ # @return [Pending, nil]
235
+ def pending_for(subject)
236
+ pull = open_pull(subject) or return nil
237
+
238
+ Pending.new(plan: subject.feature.ordinal.to_s,
239
+ subject:,
240
+ round: Round.latest(subject.feature.path),
241
+ pull:,
242
+ checkout: @worktree.checkout_for(subject.feature))
243
+ rescue StandardError => e
244
+ not_settled(subject.feature.ordinal.to_s, e)
245
+ end
246
+
247
+ # On the look's thread: reads only.
248
+ #
249
+ # @return [Array(Pending, Outcome::Look), nil]
250
+ def look_at(pending)
251
+ [pending, @outcome.look(checkout: pending.checkout, plan: pending.subject, pull: pending.pull, round: pending.round)]
252
+ # StandardError, not only OpenLanes::Error: one plan's surprise from
253
+ # `gh` must not end the look for the others, nor the run.
254
+ rescue StandardError => e
255
+ not_settled(pending.plan, e)
256
+ end
257
+
258
+ # On the dispatcher's thread.
259
+ #
260
+ # @return [Settled, nil]
261
+ def apply(pending, seen, subjects)
262
+ subject = subjects.find { it.feature.ordinal.to_s == pending.plan } or return nil
263
+ return nil unless still?(pending, subject)
264
+
265
+ result = @outcome.settle(checkout: pending.checkout, plan: subject, pull: open_pull(subject), round: pending.round, seen:)
266
+ @ready << [pending.plan, pending.round.number] if result.kind == :ready
267
+ Settled.new(plan: pending.plan, result:, round: pending.round.number)
268
+ rescue StandardError => e
269
+ not_settled(pending.plan, e)
270
+ end
271
+
272
+ # @return [Boolean] whether the plan is as the look found it
273
+ def still?(pending, subject)
274
+ pull = open_pull(subject)
275
+ awaiting_ci?(subject) && Round.latest(subject.feature.path)&.number == pending.round.number &&
276
+ !pull.nil? && [pull.number, pull.url] == [pending.pull.number, pending.pull.url]
277
+ end
278
+
279
+ # @return [nil]
280
+ def not_settled(plan, error)
281
+ UI.log("verification not settled: #{error.message.lines.first.to_s.strip}", level: :warn, plan:)
282
+ nil
283
+ end
284
+
285
+ # The first look comes at once; after that, one per interval.
286
+ #
287
+ # @return [Boolean]
288
+ def due?
289
+ now = @clock.call
290
+ return false if @last && now - @last < @interval
291
+
292
+ @last = now
293
+ true
294
+ end
295
+
296
+ # Ready is remembered per round, not per plan: a plan rejected after
297
+ # `ready-to-merge`, rebuilt and verified again is a new round, and
298
+ # must be looked at again.
299
+ #
300
+ # @return [Array(String, Integer)]
301
+ def ready_key(subject) = [subject.feature.ordinal.to_s, Round.latest(subject.feature.path)&.number]
302
+
303
+ # @return [OpenLanes::Plans::PullRequest, nil]
304
+ def open_pull(subject) = subject.pull_requests.select(&:open?).last
305
+
306
+ # @return [Round]
307
+ def latest(subject) = Round.latest(subject.feature.path) || Round.next(subject.feature.path)
308
+
309
+ # @return [OpenLanes::Vcs::Worktree::Checkout]
310
+ def checkout_of(task, subject) = task.checkout || @worktree.checkout_for(subject.feature)
311
+ end
312
+ end
313
+ end
314
+ end