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,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ # The app a plan builds, run on this machine (plan 012, M12, M13): the
5
+ # processes `.openlanes.yml` declares under `app:`, started in order on
6
+ # ports from 11100, kept ready, restarted when their files change, and
7
+ # shown in the control room's Preview tab.
8
+ #
9
+ # Plural, like `Agents` and `Adapters`, so it never reads as `Web::App`,
10
+ # the control room's own Sinatra application.
11
+ module Apps
12
+ module_function
13
+
14
+ # @param config [OpenLanes::Config]
15
+ # @return [OpenLanes::Apps::Definition, nil] nil when the project declares no app
16
+ def definition(config = Config.current) = config.app
17
+
18
+ # This machine's port registry.
19
+ #
20
+ # @return [OpenLanes::Apps::Ports]
21
+ def registry(**) = Ports.new(**)
22
+
23
+ # One plan's app, started by {Supervisor#start}. Plan 014 drives this
24
+ # directly in its clean checkout; `openlanes app start` drives it
25
+ # through {.runner}.
26
+ #
27
+ # @param definition [OpenLanes::Apps::Definition]
28
+ # @param root [String] the checkout
29
+ # @param plan [String]
30
+ # @param branch [String]
31
+ # @param publisher [#call, nil]
32
+ # @return [OpenLanes::Apps::Supervisor]
33
+ def supervisor(definition:, root:, plan:, branch:, publisher: nil, **) = Supervisor.new(definition:, root:, plan:, branch:, publisher:, **)
34
+
35
+ # @return [OpenLanes::Apps::Watcher]
36
+ def watcher(**) = Watcher.new(**)
37
+
38
+ # @return [OpenLanes::Apps::Runner]
39
+ def runner(**) = Runner.new(**)
40
+ end
41
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module CLI
5
+ # `openlanes api` — the OpenAPI contracts plans carry, one `openapi.yaml`
6
+ # per plan folder. See {OpenLanes::Presentation::ApiDocs}.
7
+ module API
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module CLI
5
+ module API
6
+ # `openlanes api docs` — every plan's `openapi.yaml`, and how to read it.
7
+ #
8
+ # Read-only, so there is nothing to preview and no `--dry-run`.
9
+ class Docs < Base
10
+ desc "List the openapi.yaml files in the plan folders, and check each one parses"
11
+
12
+ example [
13
+ " # every contract, with the command that renders it",
14
+ "-D other/.plans # another plans directory"
15
+ ]
16
+
17
+ # @param options [Hash]
18
+ # @return [void]
19
+ def call(**options)
20
+ quiet = quiet?(options)
21
+ tree = tree_for(options)
22
+ result = OpenLanes::Presentation.api_docs.call(tree)
23
+
24
+ result.found.each { puts entry(it) }
25
+ # Plain `path: reason` lines rather than {UI#warn}'s box, which
26
+ # truncates the path, the one thing a person needs. This shape is
27
+ # also what an editor's quickfix list already reads.
28
+ result.problems.each { Kernel.warn("#{shown(it.path)}: #{it.reason}") }
29
+ UI.line("No #{OpenLanes::Presentation::ApiDocs::FILE} in #{tree.dir}") if result.found.empty? && result.ok? && !quiet
30
+ exit 65 unless result.ok?
31
+ end
32
+
33
+ private
34
+
35
+ # A hosted Redoc page (redocly.github.io/redoc/?url=…) only loads a
36
+ # URL the browser can fetch over HTTP, and a path on this disk is not
37
+ # one, so that link would open on an error. Redocly's own CLI renders
38
+ # the local file to one static HTML page, offline, which is the honest
39
+ # thing to point at.
40
+ #
41
+ # @param found [OpenLanes::Presentation::ApiDocs::Found]
42
+ # @return [String]
43
+ def entry(found)
44
+ path = shown(found.path)
45
+ title = [found.title, "OpenAPI #{found.version}"].compact.join(" — ")
46
+ html = File.join(File.dirname(path), "openapi.html")
47
+ "#{path} (#{title})\n npx @redocly/cli build-docs #{path.shellescape} -o #{html.shellescape}"
48
+ end
49
+
50
+ # Relative to where the command runs, the project root, so the line
51
+ # can be pasted back into a shell as it is. A `-D` outside the root
52
+ # stays absolute: a path climbing out with `../..` reads as a typo.
53
+ #
54
+ # @param path [String]
55
+ # @return [String]
56
+ def shown(path)
57
+ root = "#{Dir.pwd}/"
58
+ path.start_with?(root) ? path.delete_prefix(root) : path
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module CLI
5
+ # `openlanes app` — the app a plan builds, run on this machine for the
6
+ # control room's Preview tab (plan 012). See {OpenLanes::Apps}.
7
+ module App
8
+ # Branch names carry the plan: `<user>/NNN.MM-slug` (see {Vcs::Worktree}).
9
+ BRANCH_PLAN = %r{\A[^/]+/(\d{3}\.\d{2})-}
10
+
11
+ # The ordinal a run without one is filed under.
12
+ NO_PLAN = "000.00"
13
+
14
+ # What every subcommand needs from the registry.
15
+ module Registry
16
+ private
17
+
18
+ # @return [OpenLanes::Apps::Ports]
19
+ def registry = OpenLanes::Apps.registry
20
+
21
+ # @param plan [String, nil] as typed
22
+ # @return [String, nil] canonical, nil when nothing was typed
23
+ def ordinal_of(plan)
24
+ return nil if plan.nil?
25
+
26
+ Plans::Ordinal.parse(plan.to_s)&.to_s || refuse("#{plan} is not a plan number like 012 or 012.00", 64)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/atomic/atomic_boolean"
4
+ require "open3"
5
+
6
+ module OpenLanes
7
+ module CLI
8
+ module App
9
+ # `openlanes app start` — run a plan's app in the foreground, as
10
+ # `.openlanes.yml` `app:` declares it, until Ctrl-C, `openlanes app
11
+ # stop`, or Stop in the Preview tab.
12
+ class Start < Base
13
+ include Registry
14
+
15
+ desc "Run a plan's app from .openlanes.yml app:, for the Preview tab"
16
+
17
+ argument :plan, required: false, desc: "Which plan: NNN or NNN.MM; its worktree when it has one"
18
+
19
+ option :redis_url, desc: "Redis the control room reads (default: .openlanes.yml redis_url, else $REDIS_URL, else #{Live::DEFAULT_URL})"
20
+ option :demo,
21
+ type: :boolean,
22
+ default: false,
23
+ desc: "Publish where `openlanes web --demo` reads, beside its sample board"
24
+ option :ready_timeout, default: Apps::Supervisor::READY_TIMEOUT.to_s, desc: "Seconds each process has to become ready"
25
+ option :control_room, default: Apps::Framing::ORIGIN, desc: "The control room's origin, which the app must allow to frame it"
26
+
27
+ example [
28
+ " # this checkout's app, filed under the plan its branch names",
29
+ "012 # plan 012's app, in its worktree when it has one",
30
+ "--demo --no-quiet # beside `openlanes web --demo`"
31
+ ]
32
+
33
+ # @param plan [String, nil]
34
+ # @param options [Hash]
35
+ # @return [void]
36
+ def call(plan: nil, **options)
37
+ quiet?(options)
38
+ root = checkout(ordinal_of(plan), options)
39
+ definition = definition_in(root)
40
+ branch = branch_of(root)
41
+ ordinal = ordinal_of(plan) || branch[BRANCH_PLAN, 1] || NO_PLAN
42
+ store, keys = board(options)
43
+ runner = Apps.runner(definition:,
44
+ root:,
45
+ plan: ordinal,
46
+ branch:,
47
+ store:,
48
+ keys:,
49
+ say: ->(line) { UI.line("#{ordinal} #{line}") },
50
+ supervisor: { ready_timeout: Float(options[:ready_timeout]), origin: options[:control_room],
51
+ output: ->(name, line) { UI.line("#{name}: #{line}", bullet: "│") } })
52
+ # A trap may not take a lock, and the flag takes one.
53
+ %w[INT TERM].each { |name| Signal.trap(name) { Thread.new { runner.stopping.make_true } } }
54
+ UI.line("Starting #{ordinal}'s app in #{root} (#{branch}); Ctrl-C stops it")
55
+ runner.call
56
+ rescue Apps::Supervisor::StartFailed => e
57
+ refuse("#{ordinal}'s app did not start: #{e.message}", 70)
58
+ ensure
59
+ store&.close
60
+ end
61
+
62
+ private
63
+
64
+ # The plan's worktree when it has one, else the project root.
65
+ #
66
+ # @return [String]
67
+ def checkout(ordinal, options)
68
+ return Dir.pwd unless ordinal
69
+
70
+ subject = tree_for(options).find(Plans::Ordinal.parse(ordinal))
71
+ refuse("No plan #{ordinal} in #{OpenLanes::PLANS_DIR}", 66) unless subject
72
+ path = Vcs.worktree(root: Dir.pwd).path_for(subject.feature)
73
+ File.directory?(path) ? path : Dir.pwd
74
+ end
75
+
76
+ # The checkout's own `.openlanes.yml`: on a plan's branch, luke may
77
+ # have written `app:` there and nowhere else yet (M12).
78
+ #
79
+ # @return [OpenLanes::Apps::Definition]
80
+ def definition_in(root)
81
+ definition = Apps.definition(OpenLanes::Config.load(root))
82
+ return definition if definition
83
+
84
+ refuse("#{File.join(root, Project::FILE)} declares no app:, so there is nothing to start.", 65)
85
+ rescue OpenLanes::Error => e
86
+ refuse(e.message, 65)
87
+ end
88
+
89
+ # `symbolic-ref` rather than `rev-parse`, so a branch with no commit
90
+ # yet still has its name.
91
+ #
92
+ # @return [String]
93
+ def branch_of(root)
94
+ out, status = Open3.capture2("git", "-C", root, "symbolic-ref", "--short", "HEAD", err: File::NULL)
95
+ status.success? ? out.strip : "(no branch)"
96
+ end
97
+
98
+ # The store and keys the control room reads, or neither when Redis
99
+ # is not there: the app runs either way.
100
+ #
101
+ # @return [Array(OpenLanes::Live::Store, OpenLanes::Live::Keys), Array(nil, nil)]
102
+ def board(options)
103
+ url = Live.redis_url(option: options[:redis_url], config: OpenLanes::Config.current)
104
+ store = Live.connect(url)
105
+ keys = Live.keys(root: Dir.pwd)
106
+ [store, options[:demo] ? keys.with(repo: "demo/#{keys.repo}") : keys]
107
+ rescue Live::Unavailable => e
108
+ UI.line("#{e.message}: the app runs, but the Preview tab will not hear of it")
109
+ [nil, nil]
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module CLI
5
+ module App
6
+ # `openlanes app status` — every port an app holds on this machine,
7
+ # read from the registry, so it answers with or without Redis.
8
+ class Status < Base
9
+ include Registry
10
+
11
+ desc "List the apps running on this machine: plan, process, port, pid, checkout"
12
+
13
+ # @param options [Hash]
14
+ # @return [void]
15
+ def call(**options)
16
+ quiet?(options)
17
+ entries = registry.entries
18
+ if entries.empty?
19
+ UI.line("No app is running on this machine.") unless quiet?(options)
20
+ return
21
+ end
22
+
23
+ rows = entries.map { |port, entry| [entry["plan"], entry["process"], port.to_s, entry["owner"].to_s, entry["root"]] }
24
+ puts table([%w[PLAN PROCESS PORT PID CHECKOUT], *rows.sort])
25
+ end
26
+
27
+ private
28
+
29
+ # @param rows [Array<Array<String>>]
30
+ # @return [String]
31
+ def table(rows)
32
+ widths = rows.transpose.map { |column| column.map { it.to_s.length }.max }
33
+ rows.map { |row| row.each_with_index.map { |cell, i| cell.to_s.ljust(widths[i]) }.join(" ").rstrip }.join("\n")
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module CLI
5
+ module App
6
+ # `openlanes app stop` — TERM to the `openlanes app start` holding a
7
+ # plan's ports, which stops its processes and frees them.
8
+ class Stop < Base
9
+ include Registry
10
+
11
+ desc "Stop a plan's app (every app on this machine when no plan is given)"
12
+
13
+ argument :plan, required: false, desc: "Which plan: NNN or NNN.MM"
14
+
15
+ # @param plan [String, nil]
16
+ # @param options [Hash]
17
+ # @return [void]
18
+ def call(plan: nil, **options)
19
+ quiet?(options)
20
+ ordinal = ordinal_of(plan)
21
+ owners = registry.entries.values.select { ordinal.nil? || it["plan"] == ordinal }.map { it["owner"] }.uniq
22
+ if owners.empty?
23
+ UI.line("No app is running#{" for #{ordinal}" if ordinal} on this machine.") unless quiet?(options)
24
+ return
25
+ end
26
+
27
+ owners.each { signal(it) }
28
+ end
29
+
30
+ private
31
+
32
+ # @return [void]
33
+ def signal(pid)
34
+ Process.kill("TERM", pid)
35
+ UI.line("Asked #{pid} to stop")
36
+ rescue Errno::ESRCH
37
+ UI.line("#{pid} had already stopped")
38
+ rescue Errno::EPERM
39
+ refuse("#{pid} is not yours to stop", 77)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -144,8 +144,85 @@ module OpenLanes
144
144
  desc:
145
145
  end
146
146
 
147
+ # `-l`, `-L` and `--log-format`, for the commands that run something
148
+ # long enough to be worth a log. No defaults here: a flag left out must
149
+ # stay nil, so {Log::Settings.from} can fall back to what the engine
150
+ # handed its server through the environment.
151
+ #
152
+ # @return [void]
153
+ def self.log_options
154
+ option :log,
155
+ aliases: ["-l"],
156
+ desc: "Write the log to FILE, appending; - for STDOUT (default: #{Log::DEFAULT_FILE} under the project root)"
157
+ option :log_level,
158
+ aliases: ["-L"],
159
+ values: Log::LEVELS,
160
+ desc: "The lowest level written (default: #{Log::DEFAULT_LEVEL})"
161
+ option :log_format,
162
+ values: Log::FORMATS.keys,
163
+ desc: "color or standard (one line per entry, with or without colour) or json (default: #{Log::DEFAULT_FORMAT})"
164
+ end
165
+
166
+ # `--ips` (alias `--bind`), `-I` and `--admin-credentials`, for the
167
+ # commands that serve the control room: `web`, and `run` and `start`
168
+ # through the server they launch.
169
+ #
170
+ # @return [void]
171
+ def self.listen_options
172
+ option :ips,
173
+ aliases: ["-i", "--bind"],
174
+ desc: "Comma-separated addresses the control room listens on, e.g. 127.0.0.1,192.168.1.20 " \
175
+ "(default: #{OpenLanes::Web::HOST}). Any address past loopback needs --admin-credentials"
176
+ option :all_interfaces,
177
+ aliases: ["-I"],
178
+ type: :boolean,
179
+ default: false,
180
+ desc: "Listen on every interface, the same as --ips #{OpenLanes::Web::Listen::EVERYWHERE}"
181
+ option :admin_credentials,
182
+ desc: "USER:PASSWORD, the Basic auth login asked of every request from off this machine; required " \
183
+ "when listening past loopback (default: $#{OpenLanes::Web::AdminAuth::ENV_NAME})"
184
+ end
185
+
147
186
  private
148
187
 
188
+ # `--ips`, or `-I` for every interface. Both at once is refused rather
189
+ # than one quietly winning, since they disagree about who can reach it.
190
+ #
191
+ # @param options [Hash]
192
+ # @return [OpenLanes::Web::Listen]
193
+ def listen_for(options)
194
+ if options[:all_interfaces]
195
+ refuse("Pass --ips or -I, not both.", 64) unless options[:ips].to_s.strip.empty?
196
+ return OpenLanes::Web::Listen.parse(OpenLanes::Web::Listen::EVERYWHERE)
197
+ end
198
+ OpenLanes::Web::Listen.parse(options[:ips])
199
+ rescue OpenLanes::Web::Listen::Invalid => e
200
+ refuse("--ips: #{e.message}", 64)
201
+ end
202
+
203
+ # `--admin-credentials`, else the ones `openlanes run` hands its server
204
+ # through the environment. Listening past loopback without them is
205
+ # refused: the page can stop agents and answer for a person, and a
206
+ # server anyone on the network can drive is never a default.
207
+ #
208
+ # @param options [Hash]
209
+ # @param listen [OpenLanes::Web::Listen]
210
+ # @param env [Hash]
211
+ # @return [OpenLanes::Web::AdminAuth::Credentials, nil]
212
+ def credentials_for(options, listen, env: ENV)
213
+ text = [options[:admin_credentials], env[OpenLanes::Web::AdminAuth::ENV_NAME]].find { !it.to_s.empty? }
214
+ if text.nil?
215
+ return unless listen.public?
216
+
217
+ refuse("Listening on #{listen.public_ips.join(", ")} needs a login: pass --admin-credentials USER:PASSWORD, " \
218
+ "or set $#{OpenLanes::Web::AdminAuth::ENV_NAME}.",
219
+ 64)
220
+ end
221
+ OpenLanes::Web::AdminAuth.parse(text)
222
+ rescue OpenLanes::Web::AdminAuth::Invalid => e
223
+ refuse("--admin-credentials #{e.message}.", 64)
224
+ end
225
+
149
226
  # @param options [Hash]
150
227
  # @return [OpenLanes::Plans::Tree]
151
228
  def tree_for(options)
@@ -226,27 +303,23 @@ module OpenLanes
226
303
  # @return [Boolean]
227
304
  def dry_run?(options) = options.fetch(:dry_run, false)
228
305
 
229
- # `claude` prefers a credential in the environment to a claude.ai login,
230
- # so a project `.env` that direnv loads on `cd` can redirect every agent
231
- # a run spawns to a key meant for the application itself.
232
- #
233
- # Say it once, before the run starts, because the failure arrives three
234
- # minutes later, once per agent, and reads like an agent problem rather
235
- # than an environment one.
306
+ # The credential warning {Execution.credentials_notice} words, drawn
307
+ # when there is one to give.
236
308
  #
237
309
  # @return [void]
238
310
  def credentials_warning
239
- names = Execution::Executor.foreign_credentials
240
- return if names.empty?
241
-
242
- one = names.size == 1
243
- warn("#{names.join(" and ")} #{one ? "is" : "are"} set in this shell, so every agent authenticates " \
244
- "with #{one ? "it" : "them"} rather than with your claude.ai login.\n" \
245
- "A stale one fails every agent in the run with `401 API key is invalid`, " \
246
- "three minutes at a time.\n" \
247
- "If it came from a project .env, run `unset #{names.join(" ")}` first.")
311
+ notice = Execution.credentials_notice
312
+ warn(notice) if notice
248
313
  end
249
314
 
315
+ # The success value of a workflow's result, or the refusal it failed
316
+ # with, reported and exited on. One shape for every workflow, so a
317
+ # command reads the same whichever one it calls.
318
+ #
319
+ # @param result [Dry::Monads::Result] Success(value) or Failure({Refusal})
320
+ # @return [Object] the success value; a failure never returns
321
+ def unwrap(result) = result.value_or { |refusal| refuse(refusal.message, refusal.status) }
322
+
250
323
  # The line every dry run ends with, so nobody mistakes a preview for the
251
324
  # thing having happened.
252
325
  #
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module CLI
5
+ # `openlanes coverage PLAN` — the coverage gate (M15), by hand.
6
+ #
7
+ # Runs the declared test command in the plan's worktree, never in the main
8
+ # tree, and prints the verdict `ready-to-merge` will wait on. The verdict is
9
+ # the deliverable and goes to STDOUT; the command's own output, when the
10
+ # gate withholds, goes to STDERR. Exits 1 when the gate withholds, so a
11
+ # script or CI step can stop on it.
12
+ #
13
+ # `CLI::Coverage` shadows the library's namespace, so this file names
14
+ # `OpenLanes::Coverage` in full.
15
+ class Coverage < Base
16
+ desc "Run a plan's declared test command in its worktree and check line coverage"
17
+
18
+ argument :plan, required: true, desc: "Plan ordinal: NNN or NNN.MM"
19
+
20
+ dry_run_option "Print the declaration and where the command would run; run nothing"
21
+
22
+ example [
23
+ "013 # run .openlanes.yml's coverage.command in plan 013's worktree",
24
+ "013 --dry-run # show what would run, and where"
25
+ ]
26
+
27
+ # @param plan [String]
28
+ # @param options [Hash]
29
+ # @return [void]
30
+ def call(plan:, **options)
31
+ tree = tree_for(options)
32
+ ordinal = Plans::Ordinal.parse(plan.to_s)
33
+ subject = ordinal && tree.find(ordinal)
34
+ refuse("No plan #{plan} in #{tree.dir}.\n\nKnown: #{tree.ordinals.join(", ")}", 66) unless subject
35
+
36
+ worktree = OpenLanes::Vcs.worktree(root: File.dirname(tree.dir))
37
+ refuse("#{worktree.root} is not a git repository; the gate runs in a plan's worktree.", 66) unless worktree.repository?
38
+
39
+ return preview(worktree, subject.feature) if dry_run?(options)
40
+
41
+ verdict = OpenLanes::Engine.coverage_gate.call(checkout: worktree.checkout_for(subject.feature))
42
+ $stdout.puts(verdict.to_s)
43
+ return if verdict.passed?
44
+
45
+ warn(verdict.output.join("\n")) unless verdict.output.empty?
46
+ exit 1
47
+ end
48
+
49
+ private
50
+
51
+ # @return [void]
52
+ def preview(worktree, feature)
53
+ path = worktree.path_for(feature)
54
+ config = File.directory?(path) && OpenLanes::Coverage.config(path)
55
+ $stdout.puts("would run in #{path}")
56
+ if config
57
+ $stdout.puts(" #{config.command}", " then read #{config.source}, minimum #{format("%g%%", config.minimum)}")
58
+ else
59
+ $stdout.puts(" the declaration is read from the #{OpenLanes::Coverage::Config::SECTION}: section of " \
60
+ "#{OpenLanes::Config::FILE} there, once the worktree exists and has one")
61
+ end
62
+ dry_run_footer(1, "coverage run")
63
+ rescue OpenLanes::Coverage::Invalid => e
64
+ refuse(e.message, 65)
65
+ end
66
+ end
67
+ end
68
+ end