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,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module Lifecycle
5
+ # `openlanes unblock NNN…` without the command: resolve the plans named,
6
+ # refuse the ones that cannot be drained, say what each is waiting on,
7
+ # hand the rest to the draining agent, and report what moved.
8
+ #
9
+ # Deliberately not part of `run`. ⭕ and 🛑 are {Plans::StateMachine::SETTLED},
10
+ # so the loop never offers a blocked plan to anybody, and that is the
11
+ # property that makes the state mean anything: the plan waits for a human,
12
+ # and no agent quietly decides otherwise. Answers arriving is not a fact
13
+ # the tool can observe, so a human typing the command *is* the signal.
14
+ class UnblockWorkflow
15
+ include UI
16
+ include Dry::Monads[:result]
17
+
18
+ # The agent that knows the shape of `blocked.md`.
19
+ DEFAULT_AGENT = "lando-broker"
20
+
21
+ # What each token turned out to be, for whoever has to fix it.
22
+ PROBLEMS = {
23
+ missing: "no plan of that number",
24
+ not_blocked: "no blocked.md, so there is nothing to drain"
25
+ }.freeze
26
+
27
+ # @param tree [OpenLanes::Plans::Tree]
28
+ # @param agent [String] the name of the agent that drains
29
+ # @param root [String, nil] repository root (default: the .plans parent)
30
+ # @param dry_run [Boolean] invoke no agent, change nothing
31
+ # @param quiet [Boolean] draw nothing on STDERR
32
+ def initialize(tree:, agent: DEFAULT_AGENT, root: nil, dry_run: false, quiet: false)
33
+ @tree = tree
34
+ @agent = agent
35
+ @root = root
36
+ @dry_run = dry_run
37
+ @quiet = quiet
38
+ end
39
+
40
+ # The deliverable rows go to STDOUT and the progress to STDERR as the
41
+ # work happens; the answer is the exit status the command ends with.
42
+ #
43
+ # @param tokens [Array<String>] the plans named: NNN or NNN.MM
44
+ # @return [Dry::Monads::Result] Success(Integer) the exit status, or
45
+ # Failure({Refusal}) when no agent has the name given
46
+ def call(tokens)
47
+ agent = OpenLanes::Agents.registry.find(@agent) or return Failure(unknown_agent)
48
+
49
+ unblocker = Lifecycle.unblocker(tree: @tree,
50
+ agent:,
51
+ root: @root,
52
+ commit: !@dry_run,
53
+ executor: Execution.executor(root: @root || File.dirname(@tree.dir), dry_run: @dry_run))
54
+ targets = unblocker.resolve(tokens)
55
+ refused(targets)
56
+ subjects = targets.select(&:drainable?).map(&:subject)
57
+ return Success(worst(targets, [])) if subjects.empty?
58
+
59
+ credentials unless @dry_run || @quiet
60
+ preflight(subjects)
61
+
62
+ outcomes = Execution.on_interrupt { unblocker.call(subjects) }
63
+ outcomes.each { |outcome| report(outcome) }
64
+ footer(outcomes) unless @quiet
65
+ Success(worst(targets, outcomes))
66
+ end
67
+
68
+ private
69
+
70
+ # @return [OpenLanes::Refusal]
71
+ def unknown_agent
72
+ known = OpenLanes::Agents.registry.all.map(&:name).join(", ")
73
+ Refusal.new(message: "No agent called #{@agent}.\n\nKnown: #{known}", status: 65)
74
+ end
75
+
76
+ # @return [void]
77
+ def credentials
78
+ notice = Execution.credentials_notice
79
+ warn(notice) if notice
80
+ end
81
+
82
+ # A number that names no folder, or a folder that was never stopped, is
83
+ # refused rather than skipped. Whoever typed this believes an answer has
84
+ # arrived; running against a folder nobody blocked and saying nothing is
85
+ # how you come back later to a plan nobody touched and no record of why.
86
+ #
87
+ # Every bad token is reported, not just the first, because stopping at
88
+ # the first one hides the rest of the answer.
89
+ #
90
+ # @param targets [Array<OpenLanes::Lifecycle::Unblocker::Target>]
91
+ # @return [void]
92
+ def refused(targets)
93
+ problems = targets.reject(&:drainable?)
94
+ return if problems.empty?
95
+
96
+ problems.each do |target|
97
+ state = target.subject ? target.subject.status.key : "unknown"
98
+ puts "#{target.token}\t#{state}\t0 open\t#{PROBLEMS.fetch(target.problem)}"
99
+ end
100
+ return if @quiet
101
+
102
+ error("#{problems.size} of the plans named cannot be drained:\n\n" +
103
+ problems.map { |t| " #{t.token} — #{PROBLEMS.fetch(t.problem)}" }.join("\n") +
104
+ "\n\nThe tree holds: #{@tree.ordinals.join(", ")}")
105
+ end
106
+
107
+ # What each folder is waiting on, said BEFORE the agent is invoked.
108
+ #
109
+ # A real run hands the folder to an agent that can take a quarter of an
110
+ # hour, and until it came back the terminal used to show nothing at
111
+ # all: a run that had found nothing to do and a run still working
112
+ # looked exactly alike.
113
+ #
114
+ # @param subjects [Array<OpenLanes::Plans::Subject>]
115
+ # @return [void]
116
+ def preflight(subjects)
117
+ return if @quiet
118
+
119
+ subjects.each do |subject|
120
+ questions = Unblocker.questions(subject)
121
+ answered = questions.count(&:answered)
122
+ say("#{paint(subject.feature.ordinal.to_s, :bright_black)} #{subject.status.emoji} " \
123
+ "#{subject.feature.title} — #{summary(questions.size, answered)}")
124
+ detail(subject, questions)
125
+ end
126
+ end
127
+
128
+ # @param open [Integer]
129
+ # @param answered [Integer]
130
+ # @return [String]
131
+ def summary(open, answered)
132
+ return "nothing open" if open.zero?
133
+
134
+ "#{open} open, #{answered.zero? ? "none answered yet" : "#{answered} with an answer waiting"}"
135
+ end
136
+
137
+ # @param subject [OpenLanes::Plans::Subject]
138
+ # @param questions [Array<OpenLanes::Lifecycle::Unblocker::Question>]
139
+ # @return [void]
140
+ def detail(subject, questions)
141
+ if subject.unreadable_block?
142
+ say(" #{paint("blocked.md names no `## B<n>` question, so nothing here can be drained", :red)}",
143
+ bullet: " ")
144
+ say(" #{paint("Number each open question `## B1`, `## B2`, and each answer `## A1`, `## A2`.", :yellow)}",
145
+ bullet: " ")
146
+ elsif questions.empty?
147
+ say(" #{paint("nothing left open", :green)}", bullet: " ")
148
+ else
149
+ questions.each { |question| say(" #{paint(question.to_s, :yellow)}", bullet: " ") }
150
+ end
151
+ end
152
+
153
+ # One plan, after the fact: the deliverable line on STDOUT, and what
154
+ # changed on STDERR.
155
+ #
156
+ # @param outcome [OpenLanes::Lifecycle::Unblocker::Outcome]
157
+ # @return [void]
158
+ def report(outcome)
159
+ moved = movement(outcome)
160
+ puts "#{outcome.ordinal}\t#{outcome.ok ? outcome.subject.status.key : "failed"}\t" \
161
+ "#{outcome.after.size} open\t#{moved}\t#{outcome.note}"
162
+ # A dry run changed nothing, so the preflight above it is still the
163
+ # whole truth. Printing the same lines again reads as a second pass
164
+ # that found the same thing, which is not what happened.
165
+ return if @quiet || @dry_run
166
+
167
+ say("#{paint(outcome.ordinal.to_s, :bright_black)} #{outcome.subject.status.emoji} " \
168
+ "#{outcome.subject.feature.title} — #{paint(moved, outcome.ok ? :green : :red)}")
169
+ detail(outcome.subject, outcome.after)
170
+ end
171
+
172
+ # What the file says happened, rather than what the agent claims. The
173
+ # questions are counted off disk on both sides of the run.
174
+ #
175
+ # @param outcome [OpenLanes::Lifecycle::Unblocker::Outcome]
176
+ # @return [String]
177
+ def movement(outcome)
178
+ return "failed" unless outcome.ok
179
+ return "not attempted" if @dry_run
180
+ return "blocked.md retired" if outcome.cleared?
181
+ return "nothing folded" if outcome.folded.empty?
182
+
183
+ "folded #{outcome.folded.map { |n| "B#{n}" }.join(", ")}"
184
+ end
185
+
186
+ # @param outcomes [Array<OpenLanes::Lifecycle::Unblocker::Outcome>]
187
+ # @return [void]
188
+ def footer(outcomes)
189
+ if @dry_run
190
+ warn("Dry run: no agent was invoked, and #{outcomes.size} " \
191
+ "plan#{"s" unless outcomes.size == 1} #{outcomes.size == 1 ? "is" : "are"} unchanged.\n" \
192
+ "Re-run without --dry-run to fold in whatever has been answered.")
193
+ return
194
+ end
195
+
196
+ failed = outcomes.reject(&:ok)
197
+ return error(failed.map { |o| "#{o.ordinal}: #{o.note}" }.join("\n")) unless failed.empty?
198
+
199
+ success(tally(outcomes))
200
+ end
201
+
202
+ # @param outcomes [Array<OpenLanes::Lifecycle::Unblocker::Outcome>]
203
+ # @return [String]
204
+ def tally(outcomes)
205
+ cleared = outcomes.count(&:cleared?)
206
+ folded = outcomes.sum { |o| o.folded.size }
207
+ waiting = outcomes.sum { |o| o.after.size }
208
+ text = "#{folded} question#{"s" unless folded == 1} folded in · " \
209
+ "#{cleared} plan#{"s" unless cleared == 1} out of the block · " \
210
+ "#{waiting} still waiting on a human."
211
+ return text unless folded.zero?
212
+
213
+ "#{text}\n\nNothing moved. An answer has to be written into blocked.md as its own " \
214
+ "`## A<n>` section, answering the `## B<n>` of the same number, before " \
215
+ "there is anything to fold."
216
+ end
217
+
218
+ # The most serious thing that happened, as an exit status. A tree that
219
+ # was partly drained still exits non-zero when part of it could not be.
220
+ #
221
+ # @param targets [Array<OpenLanes::Lifecycle::Unblocker::Target>]
222
+ # @param outcomes [Array<OpenLanes::Lifecycle::Unblocker::Outcome>]
223
+ # @return [Integer]
224
+ def worst(targets, outcomes)
225
+ problems = targets.map(&:problem)
226
+ return 66 if problems.include?(:missing)
227
+ return 65 if problems.include?(:not_blocked)
228
+ return 1 unless outcomes.all?(&:ok)
229
+
230
+ 0
231
+ end
232
+ end
233
+ end
234
+ end
@@ -118,7 +118,7 @@ module OpenLanes
118
118
  # is as much a thing the user needs told about as a folder that drained, and
119
119
  # stopping at the first bad one hides the rest of the report.
120
120
  #
121
- # The gate is `blocked.md` itself, not the folder's emoji. ⭕️ is *derived*
121
+ # The gate is `blocked.md` itself, not the folder's emoji. ⭕ is *derived*
122
122
  # from the file, so gating on the emoji made the tool circular: a
123
123
  # `blocked.md` whose questions are not numbered `## B<n>` never earns the
124
124
  # emoji, so `unblock` refused it, so the file could never drain and the
@@ -187,11 +187,85 @@ module OpenLanes
187
187
  # @param progress [OpenLanes::UI::Line]
188
188
  # @return [Array(Boolean, String)]
189
189
  def invoke(subject, progress)
190
- @executor.call(agent, subject, root:, &progress)
190
+ journal_answers(subject)
191
+ forward = progress.to_proc
192
+ kept = { written: 0, number: nil }
193
+ @executor.call(agent, subject, root:) do |update|
194
+ record_journal(subject, update, kept)
195
+ forward.call(update)
196
+ end
191
197
  rescue StandardError => e
192
198
  [false, "#{e.class}: #{e.message.lines.first.to_s.strip}"]
193
199
  end
194
200
 
201
+ # The broker's own journal lines (M2), written as they arrive, the way
202
+ # {Engine::Dispatcher} writes every other agent's: its prompt asks it to
203
+ # keep one like any agent's, and neither the run loop nor `openlanes
204
+ # unblock` hands its progress to the dispatcher. As they arrive, so a
205
+ # broker that fails or is killed keeps what it said.
206
+ #
207
+ # A journal that cannot be written costs the record, never the drain;
208
+ # the lines stay unwritten for the next update to try again.
209
+ #
210
+ # @param subject [OpenLanes::Plans::Subject]
211
+ # @param update [Object] whatever the executor reports
212
+ # @param kept [Hash] how many lines are written, and the journal round
213
+ # @return [void]
214
+ def record_journal(subject, update, kept)
215
+ lines = update.respond_to?(:journal) ? Array(update.journal) : []
216
+ return if !applying? || lines.size <= kept[:written]
217
+
218
+ entries, rejected = lines[kept[:written]..].partition { it.is_a?(Plans::Journal::Entry) }
219
+ unless entries.empty?
220
+ kept[:number], refused = Plans::PlanState.for(subject.feature.path)
221
+ .journal!(agent: agent.name,
222
+ entries:,
223
+ number: kept[:number],
224
+ state: subject.feature.status)
225
+ rejected += refused
226
+ end
227
+ kept[:written] = lines.size
228
+ rejected.each { UI.log("journal line ignored, #{it.reason}: #{it.line[0, 160]}", level: :warn, **log_fields(subject)) }
229
+ rescue SystemCallError, OpenLanes::Error => e
230
+ UI.log("journal not written yet: #{e.message}", level: :warn, **log_fields(subject))
231
+ end
232
+
233
+ # Every answered question goes into the journal of the agent that asked
234
+ # it (M3), B1 with A1, before the broker folds it in and deletes it:
235
+ # afterwards the words are gone from `blocked.md`, and the asker's next
236
+ # round would know only that the plan was unblocked, not what it was
237
+ # told. Here, rather than where the page writes an answer, because an
238
+ # answer typed into the file by hand is drained the same way.
239
+ #
240
+ # A journal that cannot be written costs the record, never the drain.
241
+ #
242
+ # @param subject [OpenLanes::Plans::Subject]
243
+ # @return [void]
244
+ def journal_answers(subject)
245
+ return unless applying?
246
+
247
+ state = Plans::PlanState.for(subject.feature.path)
248
+ asker = state.blocked_by or return
249
+ text = subject.read("blocked.md").to_s
250
+ bodies = Plans::BlockedFile.bodies(text)
251
+ answers = Plans::BlockedFile.answers(text).transform_values { plain(it) }
252
+ self.class.questions(subject).each do |question|
253
+ answer = answers[question.number]
254
+ next if answer.nil? || answer.empty?
255
+
256
+ state.answer!(agent: asker,
257
+ ref: "B#{question.number}",
258
+ question: plain("#{question.heading} #{bodies[question.number]}"),
259
+ answer:)
260
+ end
261
+ rescue SystemCallError, OpenLanes::Error => e
262
+ UI.log("journal not written: #{e.message}", level: :warn, **log_fields(subject))
263
+ end
264
+
265
+ # @param text [String] a block's body
266
+ # @return [String] one line, without the page's command markers
267
+ def plain(text) = Plans::Journal.clip(text.gsub(/<!--.*?-->/m, "").split.join(" "))
268
+
195
269
  # The same pass the run loop makes after every round, for the same reason:
196
270
  # a drained folder is only *named* differently once something reads the
197
271
  # file the agent just deleted.
@@ -3,7 +3,8 @@
3
3
  module OpenLanes
4
4
  # Operations that change which plans exist or what they are called:
5
5
  # minting a folder, drafting its brief, adopting an orphan pull request,
6
- # reconciling names with contents, and draining answered questions.
6
+ # reconciling names with contents, draining answered questions, and
7
+ # aging the ones nobody answers.
7
8
  module Lifecycle
8
9
  module_function
9
10
 
@@ -12,6 +13,10 @@ module OpenLanes
12
13
  # @return [Dry::Monads::Result] Success(path) or Failure(message)
13
14
  def create(dir:, **) = Creator.new(dir:).create(**)
14
15
 
16
+ # @param options [Hash] passed to {CreateWorkflow#initialize}
17
+ # @return [OpenLanes::Lifecycle::CreateWorkflow] `openlanes create`, without the command
18
+ def create_plan(**) = CreateWorkflow.new(**)
19
+
15
20
  # @param tree [OpenLanes::Plans::Tree]
16
21
  # @return [OpenLanes::Lifecycle::Resync::Dirs]
17
22
  def resync_dirs(tree:, **) = Resync::Dirs.new(tree:, **)
@@ -19,10 +24,41 @@ module OpenLanes
19
24
  # @return [OpenLanes::Lifecycle::Resync::Prs]
20
25
  def resync_prs(**) = Resync::Prs.new(**)
21
26
 
27
+ # `resync prs` as one step: retitle (and adopt) on GitHub, or say why not.
28
+ #
29
+ # @param commit [Boolean] false previews and changes nothing
30
+ # @param options [Hash] passed to {Resync::Prs#initialize}
31
+ # @return [Dry::Monads::Result] Success(Array<Resync::Prs::Change>), or
32
+ # Failure({Refusal}) with status 69 when GitHub refused, or 73 when an
33
+ # orphan would be adopted in a linked worktree, where no plan is minted
34
+ def retitle_prs(commit:, **)
35
+ Dry::Monads::Result::Success.new(Resync::Prs.new(**).call(commit:))
36
+ rescue Allocator::Refused => e
37
+ Dry::Monads::Result::Failure.new(Refusal.new(message: e.message, status: 73))
38
+ rescue OpenLanes::Error => e
39
+ Dry::Monads::Result::Failure.new(Refusal.new(message: e.message, status: 69))
40
+ end
41
+
22
42
  # @return [OpenLanes::Lifecycle::Brief]
23
43
  def brief(**) = Brief.new(**)
24
44
 
45
+ # @param options [Hash] passed to {UnblockWorkflow#initialize}
46
+ # @return [OpenLanes::Lifecycle::UnblockWorkflow] `openlanes unblock`, without the command
47
+ def unblock(**) = UnblockWorkflow.new(**)
48
+
25
49
  # @return [OpenLanes::Lifecycle::Unblocker]
26
50
  def unblocker(**) = Unblocker.new(**)
51
+
52
+ # Bomb or defer the blocked plans past their limits (plan 009).
53
+ #
54
+ # @param tree [OpenLanes::Plans::Tree]
55
+ # @param subjects [Array<OpenLanes::Plans::Subject>, nil] nil for every plan
56
+ # @param since [Date, nil] `plans.aging_since`
57
+ # @param now [Time]
58
+ # @param commit [Boolean] false reports and changes nothing
59
+ # @return [OpenLanes::Lifecycle::Aging::Result]
60
+ def age(tree:, subjects: nil, since: nil, now: Time.now, commit: true)
61
+ Aging.new(tree:, since:, now:).call(subjects:, commit:)
62
+ end
27
63
  end
28
64
  end
@@ -12,7 +12,7 @@ module OpenLanes
12
12
  # contract, and the name we would prefer if the team happens to have one.
13
13
  # {Push} resolves the name first and falls back to the type.
14
14
  #
15
- # Several states have no Linear equivalent at all — ⭕️ Technical Block is
15
+ # Several states have no Linear equivalent at all — ⭕ Technical Block is
16
16
  # a reason, not a position in a workflow — so they map to the nearest
17
17
  # position and carry a label saying which reason.
18
18
  #
@@ -50,11 +50,14 @@ module OpenLanes
50
50
  building_ui: Placement.new(type: "started", name: "In Progress", labels: %w[frontend]),
51
51
  ready_for_review: Placement.new(type: "started", name: "In Review", labels: []),
52
52
  in_review: Placement.new(type: "started", name: "In Review", labels: []),
53
+ verifying: Placement.new(type: "started", name: "In Review", labels: []),
54
+ completed: Placement.new(type: "started", name: "In Review", labels: %w[ready-to-merge]),
53
55
  rejected: Placement.new(type: "started", name: "In Review", labels: %w[changes-requested]),
54
56
  approved: Placement.new(type: "completed", name: "Done", labels: []),
55
57
  deployed: Placement.new(type: "completed", name: "Done", labels: %w[deployed]),
56
58
  blocked: Placement.new(type: "unstarted", name: "Todo", labels: %w[blocked]),
57
59
  product_blocked: Placement.new(type: "unstarted", name: "Todo", labels: %w[blocked-on-product]),
60
+ bomb: Placement.new(type: "unstarted", name: "Todo", labels: %w[blocked bomb]),
58
61
  deferred: Placement.new(type: "backlog", name: "Backlog", labels: %w[deferred]),
59
62
  retroactive: Placement.new(type: "completed", name: "Done", labels: %w[retroactive]),
60
63
  discarded: Placement.new(type: "canceled", name: "Canceled", labels: [])
@@ -55,7 +55,7 @@ module OpenLanes
55
55
  # the moment the work moves.
56
56
  GLYPH_NOTE = /\s*\((?:[^\w\s(][^)]*)\)\s*\z/
57
57
 
58
- # The same thing without the brackets: `… over the law plane ☢️ deferred`.
58
+ # The same thing without the brackets: `… over the law plane 💤 deferred`.
59
59
  TRAILING_GLYPH = /\s*[\u{2190}-\u{2BFF}\u{1F000}-\u{1FAFF}][^\n]*\z/
60
60
 
61
61
  # A plan number the author typed into the heading as well. The issue for