openlanes 3.0.0 → 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 (329) 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 +62 -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 +24 -3
  65. data/lib/openlanes/adapters/capabilities.rb +29 -0
  66. data/lib/openlanes/adapters/claude.rb +32 -10
  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 +26 -2
  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 -454
  106. data/lib/openlanes/cli/setup/setup.rb +51 -0
  107. data/lib/openlanes/cli/start/start.rb +7 -64
  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 +94 -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 +1229 -116
  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 +107 -0
  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 +38 -105
  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 +236 -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 +39 -11
  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 +665 -0
  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 +221 -31
  182. data/lib/openlanes/live/copilot/brief.rb +128 -0
  183. data/lib/openlanes/live/copilot/cards.rb +226 -0
  184. data/lib/openlanes/live/copilot/operator.rb +259 -0
  185. data/lib/openlanes/live/copilot/slash.rb +82 -0
  186. data/lib/openlanes/live/copilot.rb +290 -0
  187. data/lib/openlanes/live/event_bus/base.rb +52 -0
  188. data/lib/openlanes/live/event_bus/memory_bus.rb +14 -0
  189. data/lib/openlanes/live/event_bus/redis_bus.rb +10 -0
  190. data/lib/openlanes/live/event_bus/transaction.rb +27 -0
  191. data/lib/openlanes/live/event_bus.rb +29 -0
  192. data/lib/openlanes/live/interviewer.rb +146 -0
  193. data/lib/openlanes/live/keys.rb +114 -0
  194. data/lib/openlanes/live/mail.rb +143 -0
  195. data/lib/openlanes/live/memory_store.rb +36 -1
  196. data/lib/openlanes/live/memory_subscriber.rb +40 -0
  197. data/lib/openlanes/live/news.rb +61 -0
  198. data/lib/openlanes/live/node.rb +20 -0
  199. data/lib/openlanes/live/outage.rb +225 -0
  200. data/lib/openlanes/live/outage_report.rb +89 -0
  201. data/lib/openlanes/live/publisher.rb +42 -2
  202. data/lib/openlanes/live/run_record.rb +39 -1
  203. data/lib/openlanes/live/run_state.rb +19 -2
  204. data/lib/openlanes/live/session.rb +38 -4
  205. data/lib/openlanes/live/snapshot.rb +103 -42
  206. data/lib/openlanes/live/stations.rb +12 -8
  207. data/lib/openlanes/live/store.rb +36 -1
  208. data/lib/openlanes/live/subscriber.rb +93 -0
  209. data/lib/openlanes/live.rb +71 -1
  210. data/lib/openlanes/plans/blocked_file.rb +117 -0
  211. data/lib/openlanes/plans/choices.rb +228 -0
  212. data/lib/openlanes/plans/dependencies.rb +304 -0
  213. data/lib/openlanes/plans/feature.rb +75 -16
  214. data/lib/openlanes/plans/journal.rb +169 -0
  215. data/lib/openlanes/plans/last_known_state.rb +82 -0
  216. data/lib/openlanes/plans/lock.rb +171 -0
  217. data/lib/openlanes/plans/mailbox.rb +145 -4
  218. data/lib/openlanes/plans/plan_state.rb +590 -9
  219. data/lib/openlanes/plans/pull_request_machine.rb +163 -0
  220. data/lib/openlanes/plans/review_label.rb +139 -0
  221. data/lib/openlanes/plans/spec.rb +239 -3
  222. data/lib/openlanes/plans/state_machine.rb +63 -30
  223. data/lib/openlanes/plans/status.rb +60 -11
  224. data/lib/openlanes/plans/subject.rb +4 -0
  225. data/lib/openlanes/plans/tree.rb +101 -10
  226. data/lib/openlanes/plans/verdict.rb +124 -0
  227. data/lib/openlanes/plans.rb +19 -0
  228. data/lib/openlanes/presentation/api_docs.rb +76 -0
  229. data/lib/openlanes/presentation/documentation.rb +105 -12
  230. data/lib/openlanes/presentation/pr_changes.rb +90 -0
  231. data/lib/openlanes/presentation/reporter.rb +66 -7
  232. data/lib/openlanes/presentation.rb +20 -1
  233. data/lib/openlanes/setup/check.rb +16 -0
  234. data/lib/openlanes/setup/machine.rb +179 -0
  235. data/lib/openlanes/setup.rb +34 -0
  236. data/lib/openlanes/support/clock.rb +57 -0
  237. data/lib/openlanes/support/config.rb +318 -6
  238. data/lib/openlanes/support/log.rb +206 -0
  239. data/lib/openlanes/support/refusal.rb +15 -0
  240. data/lib/openlanes/support/runtime.rb +61 -0
  241. data/lib/openlanes/support/spawner.rb +51 -0
  242. data/lib/openlanes/support/ui.rb +110 -21
  243. data/lib/openlanes/vcs/ci_gate.rb +31 -0
  244. data/lib/openlanes/vcs/github.rb +362 -12
  245. data/lib/openlanes/vcs/label_migration.rb +132 -0
  246. data/lib/openlanes/vcs/label_sync.rb +75 -0
  247. data/lib/openlanes/vcs/publisher.rb +287 -7
  248. data/lib/openlanes/vcs/refresh.rb +337 -0
  249. data/lib/openlanes/vcs/rename_date.rb +83 -0
  250. data/lib/openlanes/vcs/review.rb +66 -0
  251. data/lib/openlanes/vcs/worktree.rb +210 -14
  252. data/lib/openlanes/vcs.rb +18 -0
  253. data/lib/openlanes/version.rb +1 -1
  254. data/lib/openlanes/web/admin_auth.rb +104 -0
  255. data/lib/openlanes/web/app.rb +213 -15
  256. data/lib/openlanes/web/context.rb +33 -9
  257. data/lib/openlanes/web/demo.rb +39 -14
  258. data/lib/openlanes/web/demo_board.rb +63 -5
  259. data/lib/openlanes/web/events.rb +137 -9
  260. data/lib/openlanes/web/launcher.rb +70 -8
  261. data/lib/openlanes/web/listen.rb +133 -0
  262. data/lib/openlanes/web/plan_files.rb +136 -0
  263. data/lib/openlanes/web/request_log.rb +108 -0
  264. data/lib/openlanes/web/server.rb +31 -10
  265. data/lib/openlanes/web/stores.rb +11 -0
  266. data/lib/openlanes/web.rb +19 -4
  267. data/lib/openlanes.rb +45 -7
  268. data/openlanes.gemspec +9 -4
  269. data/schemas/journal-event.schema.json +72 -0
  270. data/schemas/plan-state.schema.json +163 -2
  271. data/skills/openlanes/SKILL.md +2 -2
  272. data/skills/openlanes-documents/SKILL.md +4 -4
  273. data/skills/openlanes-documents/references/blocked-template.md +2 -2
  274. data/skills/openlanes-documents/references/closing-templates.md +1 -1
  275. data/skills/openlanes-documents/references/plan-template.md +12 -4
  276. data/skills/openlanes-documents/references/retroactive-spec.md +1 -1
  277. data/skills/openlanes-run/SKILL.md +5 -5
  278. data/skills/openlanes-spec/SKILL.md +1 -1
  279. data/skills/openlanes-status/SKILL.md +1 -1
  280. data/web/dist/assets/favicon-BFJEKKAE.png +0 -0
  281. data/web/dist/assets/{index-Bp7W-ZTq.js → index-690ayLS4.js} +2 -2
  282. data/web/dist/assets/index-6ZmW4slI.js +199 -0
  283. data/web/dist/assets/{index-Byyxbh_z.js → index-B5KzlXp1.js} +2 -2
  284. data/web/dist/assets/{index-8dNhRmiO.js → index-BAo_v9bU.js} +2 -2
  285. data/web/dist/assets/{index-2xPC-eHt.js → index-BDm7g-xG.js} +2 -2
  286. data/web/dist/assets/{index-D0rmJN3h.js → index-BFH9bC7v.js} +2 -2
  287. data/web/dist/assets/{index-OR-odsl9.js → index-BVHPV7z4.js} +2 -2
  288. data/web/dist/assets/{index-C0VmkrO-.js → index-B_CPqjnA.js} +2 -2
  289. data/web/dist/assets/{index-DDI7zoAS.js → index-BhwBH5ct.js} +2 -2
  290. data/web/dist/assets/{index-BdR34cXC.js → index-Bq5AdwEt.js} +2 -2
  291. data/web/dist/assets/{index-uM88Njdj.js → index-CAW6ClM2.js} +2 -2
  292. data/web/dist/assets/index-Cp86u27u.css +1 -0
  293. data/web/dist/assets/{index-P3vSkiU-.js → index-Culm4pMO.js} +2 -2
  294. data/web/dist/assets/{index-DGLUuEXi.js → index-DElDJfp1.js} +2 -2
  295. data/web/dist/assets/{index-CYxcbpRM.js → index-Dja7osNW.js} +2 -2
  296. data/web/dist/assets/{index-popk9wbW.js → index-DjhU3FzU.js} +2 -2
  297. data/web/dist/assets/{index-mRV4XhJZ.js → index-Dutbxj4o.js} +2 -2
  298. data/web/dist/assets/{index-DB4QwuYn.js → index-RkDhPMDt.js} +2 -2
  299. data/web/dist/assets/{index-B7-O9ujQ.js → index-_mptZjlC.js} +2 -2
  300. data/web/dist/assets/logo-DXDCUiD8.png +0 -0
  301. data/web/dist/index.html +3 -3
  302. data/web/src/api/fixtures/board.json +203 -28
  303. metadata +1053 -881
  304. data/bin/factory-e2e +0 -157
  305. data/bin/plan-number +0 -164
  306. data/bin/setup +0 -26
  307. data/evals/cases/palpatine-planner/plan-deep/fail/plan/plan.md +0 -8
  308. data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan.md +0 -12
  309. data/web/dist/assets/favicon-C8pAuqqr.png +0 -0
  310. data/web/dist/assets/index-CI74IkWP.js +0 -198
  311. data/web/dist/assets/index-a4EHR-u4.css +0 -1
  312. data/web/dist/assets/logo-on-dark-DI9gCdN4.png +0 -0
  313. data/web/dist/assets/logo-on-light-BciwqxcJ.png +0 -0
  314. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/plan/plan-backend.md +0 -0
  315. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/plan/spec.md +0 -0
  316. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/run.json +0 -0
  317. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan-backend.md +0 -0
  318. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan-frontend.md +0 -0
  319. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/spec.md +0 -0
  320. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/run.json +0 -0
  321. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/plan-backend.md +0 -0
  322. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/plan-frontend.md +0 -0
  323. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/spec.md +0 -0
  324. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/run.json +0 -0
  325. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan-backend.md +0 -0
  326. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan-frontend.md +0 -0
  327. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/spec.md +0 -0
  328. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/run.json +0 -0
  329. /data/exe/{depot → ol} +0 -0
@@ -0,0 +1,337 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module OpenLanes
6
+ module Vcs
7
+ # Brings one plan's worktree up to date with the default branch on
8
+ # `origin`, before an agent runs in it.
9
+ #
10
+ # A worktree reused round after round never moved: plan 008.01's branch
11
+ # had no commits of its own and sat 211 commits behind origin/main, so
12
+ # luke-backend went looking for code that had landed weeks earlier and
13
+ # could not find it. The agent could not catch up on its own either,
14
+ # because the boundary fails every run in which HEAD moved. So the
15
+ # harness moves HEAD, here, before {Execution::Boundary} takes its
16
+ # snapshot, and the agent's HEAD check still holds.
17
+ #
18
+ # Three cases, by what the branch carries:
19
+ #
20
+ # - nothing of its own: fast-forward. No agent is ever involved.
21
+ # - commits of its own, a clean tree: `git rebase`. A conflict leaves the
22
+ # rebase stopped for an agent to resolve; the harness continues it.
23
+ # - uncommitted changes and commits of its own: left alone. Rebasing
24
+ # needs a clean tree, and `--autostash` can strand an agent's
25
+ # uncommitted work in the stash when it fails to re-apply, which is
26
+ # exactly the silent loss worktrees exist to prevent.
27
+ #
28
+ # Nothing here commits anything of its own; `rebase --continue` replays
29
+ # commits that already exist, with the resolution an agent staged.
30
+ class Refresh
31
+ # The remote every plan branch is published to.
32
+ REMOTE = "origin"
33
+
34
+ # The default branch when `origin/HEAD` names none, as in a clone made
35
+ # with `git init` and `remote add` rather than `git clone`.
36
+ FALLBACK = "main"
37
+
38
+ # How git says a staged file still holds a conflict marker. `diff
39
+ # --check` also reports whitespace, which is no reason to stop a rebase.
40
+ MARKER = "leftover conflict marker"
41
+
42
+ # Seconds a fetch may take before it is abandoned. It runs on the
43
+ # dispatcher's tick thread, so a fetch stuck on a credential prompt or
44
+ # a stalled socket would stop every lane on the machine.
45
+ FETCH_TIMEOUT = 60
46
+
47
+ # git and ssh must fail rather than ask: nobody is there to answer.
48
+ # BatchMode is appended to the user's own ssh command, not put in its
49
+ # place, so a configured key or jump host still applies.
50
+ #
51
+ # @return [Hash{String => String}]
52
+ def self.fetch_env
53
+ { "GIT_TERMINAL_PROMPT" => "0", "GIT_SSH_COMMAND" => "#{ENV.fetch("GIT_SSH_COMMAND", "ssh")} -o BatchMode=yes" }
54
+ end
55
+
56
+ # What one refresh did.
57
+ #
58
+ # @!attribute [r] kind
59
+ # @return [Symbol] :current, :fast_forwarded, :rebased, :conflicted,
60
+ # :skipped, :pending or :aborted. Only :conflicted leaves a rebase
61
+ # stopped; a :skipped from {#continue} still has one, for the
62
+ # caller to abort
63
+ # @!attribute [r] base
64
+ # @return [String, nil] e.g. "origin/main"
65
+ # @!attribute [r] target
66
+ # @return [String, nil] the commit `base` named, so a retry can tell
67
+ # whether the default branch has moved since
68
+ # @!attribute [r] commits
69
+ # @return [Integer] how many commits the branch was behind
70
+ # @!attribute [r] files
71
+ # @return [Array<String>] the conflicted paths, when it stopped
72
+ # @!attribute [r] reason
73
+ # @return [String, nil] why nothing happened, for :skipped and :pending
74
+ Outcome = Data.define(:kind, :base, :target, :commits, :files, :reason) do
75
+ def initialize(base: nil, target: nil, commits: 0, files: [], reason: nil, **rest) = super
76
+
77
+ # @return [Boolean] the rebase is stopped, waiting on a resolution
78
+ def conflicted? = kind == :conflicted
79
+
80
+ # @return [Boolean] the branch now sits on the latest default branch
81
+ def done? = %i[current fast_forwarded rebased].include?(kind)
82
+
83
+ # One line for the progress log.
84
+ #
85
+ # @return [String]
86
+ def summary
87
+ case kind
88
+ when :current then "worktree: up to date with #{base}"
89
+ when :fast_forwarded then "worktree: fast-forwarded #{commits} commit#{"s" unless commits == 1} to #{base}"
90
+ when :rebased then "worktree: rebased onto #{base} (#{commits} new upstream commit#{"s" unless commits == 1})"
91
+ when :conflicted then "worktree: rebase onto #{base} stopped on conflicts in #{files.join(", ")}"
92
+ when :aborted then "worktree: rebase onto #{base} aborted, branch restored: #{reason}"
93
+ when :pending then "worktree: rebase onto #{base} pending: #{reason}"
94
+ else "worktree: not refreshed: #{reason}"
95
+ end
96
+ end
97
+ end
98
+
99
+ class << self
100
+ # The default branch, as `origin/HEAD` names it.
101
+ #
102
+ # @param path [String] any checkout of the repository
103
+ # @return [String] e.g. "main"
104
+ def default_branch(path)
105
+ out, status = Open3.capture2("git",
106
+ "-C",
107
+ path,
108
+ "symbolic-ref",
109
+ "--quiet",
110
+ "refs/remotes/#{REMOTE}/HEAD",
111
+ err: File::NULL)
112
+ name = out.strip.delete_prefix("refs/remotes/#{REMOTE}/")
113
+ status.success? && !name.empty? ? name : FALLBACK
114
+ end
115
+
116
+ # Fetch the default branch, and say where a new branch should start.
117
+ #
118
+ # A fetch only moves `refs/remotes`, logged as a fetch rather than a
119
+ # push, so {Execution::Boundary} does not blame an agent running in
120
+ # another worktree for it.
121
+ #
122
+ # @param path [String]
123
+ # @return [String, nil] "origin/<default>" once it exists, nil when
124
+ # the repository has no such remote branch
125
+ def fetch(path)
126
+ return nil unless system("git", "-C", path, "remote", "get-url", REMOTE, out: File::NULL, err: File::NULL)
127
+
128
+ base = default_branch(path)
129
+ fetched = fetch_within("git", "-C", path, "fetch", "--quiet", REMOTE, base)
130
+ UI.log("worktree: could not fetch #{REMOTE} #{base}; working from the last fetch") unless fetched
131
+ ref = "#{REMOTE}/#{base}"
132
+ if system("git",
133
+ "-C",
134
+ path,
135
+ "rev-parse",
136
+ "--verify",
137
+ "--quiet",
138
+ "refs/remotes/#{ref}",
139
+ out: File::NULL,
140
+ err: File::NULL)
141
+ ref
142
+ end
143
+ end
144
+
145
+ private
146
+
147
+ # Run a fetch, killing its whole process group (git, and the ssh or
148
+ # credential helper it started) once it has run past {FETCH_TIMEOUT}.
149
+ #
150
+ # @return [Boolean] whether it finished, and succeeded, in time
151
+ def fetch_within(*argv, timeout: FETCH_TIMEOUT, spawner: Spawner::System)
152
+ pid = spawner.spawn(fetch_env, *argv, pgroup: true, in: File::NULL, out: File::NULL, err: File::NULL)
153
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
154
+ loop do
155
+ _, status = Process.wait2(pid, Process::WNOHANG)
156
+ return status.success? if status
157
+ break if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
158
+
159
+ sleep 0.05
160
+ end
161
+ UI.log("worktree: git fetch timed out after #{timeout}s")
162
+ begin
163
+ Process.kill("KILL", -pid)
164
+ rescue Errno::ESRCH, Errno::EPERM
165
+ # Already gone, or not ours to end.
166
+ end
167
+ Process.wait(pid)
168
+ false
169
+ end
170
+ end
171
+
172
+ # @param path [String] the plan's worktree
173
+ def initialize(path:) = @path = path
174
+
175
+ # @return [String]
176
+ attr_reader :path
177
+
178
+ # Fetch, then fast-forward or rebase onto the default branch.
179
+ #
180
+ # @param hold [String, nil] a default-branch commit the rebase has
181
+ # already failed on as often as it may: while `origin/<default>` still
182
+ # names it, a branch that would need a rebase is left where it is
183
+ # @return [OpenLanes::Vcs::Refresh::Outcome]
184
+ def call(hold: nil)
185
+ return resume if rebasing?
186
+
187
+ base = self.class.fetch(path) or return Outcome.new(kind: :skipped, reason: "no #{REMOTE} default branch")
188
+ target = rev(base)
189
+ behind = count("HEAD..#{base}")
190
+ return Outcome.new(kind: :current, base:, target:) if behind.zero?
191
+ return fast_forward(base, target, behind) if count("#{base}..HEAD").zero?
192
+
193
+ outcome = Outcome.new(kind: :skipped, base:, target:, commits: behind)
194
+ return outcome.with(reason: "uncommitted changes in the worktree") if dirty?
195
+ return outcome.with(kind: :pending, reason: "retries exhausted until #{base} moves") if hold == target
196
+
197
+ rebase(base, target, behind)
198
+ end
199
+
200
+ # Carry a stopped rebase on, once an agent has resolved and staged.
201
+ #
202
+ # Refused while any path is unmerged or a staged file still holds a
203
+ # marker: continuing then would commit the marker into the branch.
204
+ #
205
+ # @return [OpenLanes::Vcs::Refresh::Outcome] :rebased, or :conflicted
206
+ # on the files that still need a resolution (the same commit's, or
207
+ # the next one's)
208
+ def continue
209
+ base = onto
210
+ left = unresolved
211
+ return Outcome.new(kind: :conflicted, base:, target: rev(base), files: left) unless left.empty?
212
+
213
+ # A resolution identical to what is already on HEAD leaves the commit
214
+ # empty. Newer git drops it on continue, older git refuses and is
215
+ # skipped past. Either way it drops nothing the branch still needs
216
+ # (the index is HEAD), but the commit is gone, so it is named.
217
+ empty = git({}, "diff", "--cached", "--quiet")
218
+ stopped_at = capture("log", "-1", "--format=%h %s", "REBASE_HEAD") if empty
219
+ ok = git({ "GIT_EDITOR" => "true" }, "rebase", "--continue")
220
+ ok ||= empty && rebasing? && unmerged.empty? && git({}, "rebase", "--skip")
221
+ dropped(stopped_at, base)
222
+ stopped = rebasing? ? unmerged : []
223
+ return Outcome.new(kind: :conflicted, base:, target: rev(base), files: stopped) unless stopped.empty?
224
+ return Outcome.new(kind: :rebased, base:, target: rev(base)) if ok && !rebasing?
225
+
226
+ # Stopped with nothing to resolve (a hook refused, say): no agent can
227
+ # help, so the caller aborts.
228
+ Outcome.new(kind: :skipped, base:, reason: "git rebase --continue failed with nothing unmerged")
229
+ end
230
+
231
+ # Abandon a stopped rebase. git puts the branch back exactly where it
232
+ # was before the rebase began.
233
+ #
234
+ # @param why [String]
235
+ # @return [OpenLanes::Vcs::Refresh::Outcome]
236
+ def abort!(why)
237
+ base = onto
238
+ git({}, "rebase", "--abort") if rebasing?
239
+ Outcome.new(kind: :aborted, base:, target: rev(base), reason: why)
240
+ end
241
+
242
+ # @return [Boolean] whether a rebase is stopped in this worktree
243
+ def rebasing? = %w[rebase-merge rebase-apply].any? { File.directory?(git_path(it)) }
244
+
245
+ # @return [Array<String>] paths git still lists as unmerged
246
+ def unmerged = lines("diff", "--name-only", "--diff-filter=U")
247
+
248
+ # Unmerged paths, and staged files that still hold a conflict marker.
249
+ #
250
+ # @return [Array<String>]
251
+ def unresolved
252
+ marked = lines("diff", "--cached", "--check").filter_map { |line| line.split(":", 2).first if line.include?(MARKER) }
253
+ (unmerged + marked).uniq
254
+ end
255
+
256
+ private
257
+
258
+ # A rebase found stopped before anyone asked: a harness that died
259
+ # mid-resolution, or a round that ended before it continued. It is
260
+ # picked up where it stood rather than started again.
261
+ #
262
+ # @return [OpenLanes::Vcs::Refresh::Outcome]
263
+ def resume
264
+ left = unresolved
265
+ return Outcome.new(kind: :conflicted, base: onto, target: rev(onto), files: left) unless left.empty?
266
+
267
+ continue
268
+ end
269
+
270
+ # @return [OpenLanes::Vcs::Refresh::Outcome]
271
+ def fast_forward(base, target, behind)
272
+ # Safe with uncommitted changes: git refuses, touching nothing, when
273
+ # one of them is in a file the merge would change.
274
+ return Outcome.new(kind: :fast_forwarded, base:, target:, commits: behind) if git({}, "merge", "--ff-only", "--quiet", base)
275
+
276
+ Outcome.new(kind: :skipped,
277
+ base:,
278
+ target:,
279
+ commits: behind,
280
+ reason: "fast-forward refused; uncommitted changes touch files #{base} changed")
281
+ end
282
+
283
+ # @return [OpenLanes::Vcs::Refresh::Outcome]
284
+ def rebase(base, target, behind)
285
+ return Outcome.new(kind: :rebased, base:, target:, commits: behind) if git({ "GIT_EDITOR" => "true" }, "rebase", "--quiet", base)
286
+ return Outcome.new(kind: :conflicted, base:, target:, commits: behind, files: unresolved) if rebasing?
287
+
288
+ Outcome.new(kind: :skipped, base:, target:, commits: behind, reason: "git rebase refused to start")
289
+ end
290
+
291
+ # The branch the stopped rebase is replaying onto, as a name.
292
+ #
293
+ # @return [String]
294
+ def onto
295
+ sha = File.read(git_path("rebase-merge/onto")).strip if File.file?(git_path("rebase-merge/onto"))
296
+ base = "#{REMOTE}/#{self.class.default_branch(path)}"
297
+ sha.nil? || rev(base) == sha ? base : sha[0, 7]
298
+ end
299
+
300
+ # Log a commit the rebase replayed as empty, once it is past it.
301
+ #
302
+ # @param stopped_at [String, nil] "<sha> <subject>" of the commit
303
+ # @param base [String]
304
+ # @return [void]
305
+ def dropped(stopped_at, base)
306
+ return if stopped_at.nil?
307
+ return if rebasing? && capture("log", "-1", "--format=%h %s", "REBASE_HEAD") == stopped_at
308
+
309
+ UI.log("worktree: rebase dropped #{stopped_at}: its changes are already on #{base}")
310
+ end
311
+
312
+ # @return [Boolean]
313
+ def dirty? = !lines("status", "--porcelain").empty?
314
+
315
+ # @return [Integer]
316
+ def count(range) = capture("rev-list", "--count", range).to_i
317
+
318
+ # @return [String, nil]
319
+ def rev(ref) = capture("rev-parse", "--verify", "--quiet", ref).then { it.empty? ? nil : it }
320
+
321
+ # @return [String] a path inside this worktree's own git directory
322
+ def git_path(name) = File.expand_path(capture("rev-parse", "--git-path", name), path)
323
+
324
+ # @return [Array<String>]
325
+ def lines(*) = capture(*).lines(chomp: true).reject(&:empty?)
326
+
327
+ # @return [String]
328
+ def capture(*)
329
+ out, = Open3.capture2("git", "-C", path, *, err: File::NULL)
330
+ out.strip
331
+ end
332
+
333
+ # @return [Boolean]
334
+ def git(env, *) = system(env, "git", "-C", path, *, out: File::NULL, err: File::NULL)
335
+ end
336
+ end
337
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "json"
5
+ require "open3"
6
+
7
+ module OpenLanes
8
+ module Vcs
9
+ # The date a plan reached the state that sends it into an inactive
10
+ # subfolder, for the `[YYYY-MM-DD]` its archived name carries (N3).
11
+ #
12
+ # A state change is a folder rename, so the date of the folder's latest
13
+ # rename is the date it got there. Git knows it, through a file inside the
14
+ # folder: `--follow --diff-filter=R` returns the rename itself, where
15
+ # `git log -1 -- <folder>` returns whichever edit came last, and a spec
16
+ # touched up a month after the merge would date the archive by the edit.
17
+ #
18
+ # Git does not always know. The folder may never have been committed, the
19
+ # clone may be shallow, or the tree may not be a repository at all; then
20
+ # `state.json`'s `updated_at`, which the harness writes on every state
21
+ # change, and failing that, today.
22
+ module RenameDate
23
+ # Files tried first; a folder holding none of them (a ❌ plan may hold
24
+ # only `discarded.md`) is followed through whichever file it does hold.
25
+ PROBES = %w[spec.md plan.md pull-requests.md].freeze
26
+
27
+ class << self
28
+ # @param dir [String] a plan folder
29
+ # @param today [Date] the last resort
30
+ # @return [Date]
31
+ def for(dir, today: Date.today) = from_git(dir) || from_state(dir) || today
32
+
33
+ private
34
+
35
+ # @param dir [String]
36
+ # @return [Date, nil]
37
+ def from_git(dir)
38
+ probe = probe_in(dir) or return nil
39
+
40
+ out, status = Open3.capture2("git",
41
+ "-C",
42
+ dir,
43
+ "log",
44
+ "--follow",
45
+ "--diff-filter=R",
46
+ "--format=%cI",
47
+ "-1",
48
+ "--",
49
+ probe,
50
+ err: File::NULL)
51
+ status.success? ? parse(out.strip) : nil
52
+ rescue SystemCallError
53
+ nil
54
+ end
55
+
56
+ # @param dir [String]
57
+ # @return [String, nil] a file name inside +dir+
58
+ def probe_in(dir)
59
+ PROBES.find { File.file?(File.join(dir, it)) } ||
60
+ Dir.children(dir).sort.find { File.file?(File.join(dir, it)) && it != Plans::Lock::FILENAME }
61
+ end
62
+
63
+ # @param dir [String]
64
+ # @return [Date, nil]
65
+ def from_state(dir)
66
+ parse(JSON.parse(File.read(File.join(dir, Plans::PlanState::FILENAME)))["updated_at"])
67
+ rescue SystemCallError, JSON::ParserError, TypeError
68
+ nil
69
+ end
70
+
71
+ # @param text [String, nil] an ISO-8601 timestamp
72
+ # @return [Date, nil]
73
+ def parse(text)
74
+ return nil if text.to_s.empty?
75
+
76
+ Date.iso8601(text.to_s[0, 10])
77
+ rescue Date::Error
78
+ nil
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module Vcs
5
+ # Moves a pull request from one review label to the next (M3, R3).
6
+ #
7
+ # The only way a review label is set: `gh pr edit` stays forbidden to
8
+ # agents (L3), so the CI rule below cannot be skipped by labelling by hand
9
+ # from inside a round.
10
+ class Review
11
+ # What happened, or under a dry run what would have.
12
+ Result = Data.define(:pr, :from, :to, :ci, :refused, :reason, :dry_run) do
13
+ alias_method :refused?, :refused
14
+ alias_method :dry_run?, :dry_run
15
+ end
16
+
17
+ # @param github [GitHub]
18
+ def initialize(github: GitHub.new)
19
+ @github = github
20
+ end
21
+
22
+ # @param pr [String, Integer] "12", "#12" or a pull request URL
23
+ # @param label [String] a {Plans::ReviewLabel} name
24
+ # @param dry_run [Boolean] compute the result, write nothing
25
+ # @return [Result]
26
+ # @raise [OpenLanes::Error] on an unknown label or a reference naming no
27
+ # pull request, before any `gh` call
28
+ def call(pr:, label:, dry_run: false) # rubocop:disable Naming/MethodParameterName -- `pr` is the contract with CLI::Review
29
+ to = Plans::ReviewLabel.fetch(label)
30
+ number = GitHub.number_of(pr)
31
+ repo = GitHub.repo_of(pr)
32
+
33
+ from = @github.labels(number:, repo:) & Plans::ReviewLabel.known
34
+ ci = CiGate.verdict(@github.checks(number:, repo:))
35
+ reason = refusal(to, ci)
36
+ result = Result.new(pr: number,
37
+ from:,
38
+ to: to.name,
39
+ ci:,
40
+ refused: !reason.nil?,
41
+ reason: reason || (ci == :none ? NO_CHECKS : nil),
42
+ dry_run:)
43
+ return result if result.refused? || dry_run
44
+
45
+ @github.swap_label(number:, repo:, add: to, remove: from & Plans::ReviewLabel.displaced_by(to))
46
+ result
47
+ end
48
+
49
+ # Zero checks, or only skipped ones, counts as green (open question 2),
50
+ # said out loud.
51
+ NO_CHECKS = "no CI checks ran; treated as green"
52
+
53
+ private
54
+
55
+ # @return [String, nil] why `to` may not be set now
56
+ def refusal(label, verdict)
57
+ return unless label.passing?
58
+
59
+ case verdict
60
+ when :red then "CI is red"
61
+ when :pending then "CI pending"
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end