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,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module OpenLanes
6
+ module Presentation
7
+ # The OpenAPI documents plans carry, one `openapi.yaml` per plan folder,
8
+ # gathered so a person can find and render them.
9
+ #
10
+ # This is a smoke check, not a validator: a file is a problem when it is
11
+ # not YAML, or lacks the two top-level keys every OpenAPI document has.
12
+ # A real validator would pull in a JSON Schema stack for a check whose
13
+ # job is to catch the file an agent half-wrote, which these two do.
14
+ class ApiDocs
15
+ # The name an agent writes its contract under, in its plan's folder.
16
+ FILE = "openapi.yaml"
17
+
18
+ # Keys whose absence means the file is not an OpenAPI document at all.
19
+ REQUIRED_KEYS = %w[openapi paths].freeze
20
+
21
+ # One document that passed the check.
22
+ #
23
+ # @!attribute path [String] absolute path to the file
24
+ # @!attribute title [String, nil] `info.title`, when it has one
25
+ # @!attribute version [String] the `openapi:` value, such as "3.1.0"
26
+ Found = Data.define(:path, :title, :version)
27
+
28
+ # One document that did not.
29
+ #
30
+ # @!attribute path [String] absolute path to the file
31
+ # @!attribute reason [String] what is wrong, in a sentence
32
+ Problem = Data.define(:path, :reason)
33
+
34
+ # Everything one look at a tree found.
35
+ #
36
+ # @!attribute found [Array<Found>]
37
+ # @!attribute problems [Array<Problem>]
38
+ Result = Data.define(:found, :problems) do
39
+ # @return [Boolean] true when no file failed the check
40
+ def ok? = problems.empty?
41
+ end
42
+
43
+ # @param tree [OpenLanes::Plans::Tree]
44
+ # @return [Result] files ordered by path, so plan order
45
+ def call(tree)
46
+ found = []
47
+ problems = []
48
+ Dir.glob(File.join(tree.dir, "*", FILE)).each do |path|
49
+ outcome = inspect_file(path)
50
+ (outcome.is_a?(Problem) ? problems : found) << outcome
51
+ end
52
+ Result.new(found:, problems:)
53
+ end
54
+
55
+ private
56
+
57
+ # @param path [String]
58
+ # @return [Found, Problem]
59
+ def inspect_file(path)
60
+ document = YAML.safe_load_file(path, permitted_classes: Frontmatter::PERMITTED_CLASSES, aliases: true)
61
+ return Problem.new(path:, reason: "is not a YAML mapping") unless document.is_a?(Hash)
62
+
63
+ missing = REQUIRED_KEYS.reject { document.key?(it) }
64
+ unless missing.empty?
65
+ return Problem.new(path:, reason: "has no top-level #{missing.map { "#{it}:" }.join(" or ")}")
66
+ end
67
+
68
+ Found.new(path:, title: document.dig("info", "title"), version: document["openapi"].to_s)
69
+ rescue Psych::Exception => e
70
+ # The parser's own message carries the line and column, which is
71
+ # what a person needs to find the bad indent.
72
+ Problem.new(path:, reason: "does not parse as YAML: #{e.message}")
73
+ end
74
+ end
75
+ end
76
+ end
@@ -19,6 +19,7 @@ module OpenLanes
19
19
  [
20
20
  header,
21
21
  numbering,
22
+ places,
22
23
  starting,
23
24
  status_table,
24
25
  blocking,
@@ -26,6 +27,7 @@ module OpenLanes
26
27
  diagram,
27
28
  files_section,
28
29
  pull_requests,
30
+ review_labels,
29
31
  footer
30
32
  ].join("\n")
31
33
  end
@@ -58,7 +60,7 @@ module OpenLanes
58
60
  | :---- | :---- | :---------------------- |
59
61
  | **spec** | #{status(:new).emoji} #{status(:new).label} | `spec.md` |
60
62
  | **plan** | #{status(:planned).emoji} #{status(:planned).label} | `plan.md` |
61
- | **build** | #{status(:building).emoji} → #{status(:ready_for_review).emoji} → #{status(:in_review).emoji} → #{status(:approved).emoji} | `pull-requests.md` |
63
+ | **build** | #{status(:building).emoji} → #{status(:ready_for_review).emoji} → #{status(:in_review).emoji} → #{status(:verifying).emoji} → #{status(:completed).emoji} → #{status(:approved).emoji} | `pull-requests.md` |
62
64
 
63
65
  Those files are not paperwork. They are what the tool checks: a folder may not
64
66
  claim a phase whose file is missing, and `openlanes resync dirs` renames
@@ -79,18 +81,33 @@ module OpenLanes
79
81
  The shape is always `NNN.MM`:
80
82
 
81
83
  ```
82
- #{OpenLanes::PLANS_DIR}/000.00-#{status(:new).emoji}-initial-spec
83
- #{OpenLanes::PLANS_DIR}/001.00-#{status(:approved).emoji}-dev-foundation
84
- #{OpenLanes::PLANS_DIR}/001.01-#{status(:approved).emoji}-schedule-k1-backfill <- shipped between 001 and 002,
85
- #{OpenLanes::PLANS_DIR}/002.00-#{status(:planned).emoji}-tenancy-households specified afterwards
84
+ #{OpenLanes::PLANS_DIR}/#{name("000.00", :new, "initial-spec")}
85
+ #{OpenLanes::PLANS_DIR}/#{name("001.00", :building, "dev-foundation")}
86
+ #{OpenLanes::PLANS_DIR}/#{name("001.01", :retroactive, "schedule-k1-backfill")} <- shipped between 001 and 002,
87
+ #{OpenLanes::PLANS_DIR}/#{name("002.00", :planned, "tenancy-households")} specified afterwards
86
88
  ```
87
89
 
90
+ A folder name holds no space, so it needs no quoting in a shell, and every
91
+ state emoji is a single code point, so it measures the same in every terminal.
92
+ Older spellings (`NNN.MM-<emoji> → <slug>`, `NNN-<emoji>--<slug>`) and the
93
+ emoji they used are still read; `openlanes resync dirs` renames them.
94
+
88
95
  - **`NNN` counts from `000`.** The first plan of a project is `000.00`; after
89
96
  that it is the highest major plus one, zero-padded to three digits.
90
97
  - **`MM` is `00` for an ordinary plan** — one specified before it was built.
91
98
  - **`MM` from `01` to `99` marks a retroactive plan**: work that shipped with no
92
99
  specification, documented after the fact. `openlanes create --after 001`
93
100
  takes the next free slot in the gap after 001.
101
+ - **A number is taken if any branch holds it.** "Highest" counts the primary
102
+ checkout's `#{OpenLanes::PLANS_DIR}` (archived plans included) and `#{OpenLanes::PLANS_DIR}` on every local
103
+ branch and every `origin/*` branch, read without a fetch, so a plan on an
104
+ unmerged branch keeps its number.
105
+ - **Plans are created only in the primary checkout, on its default branch**
106
+ (origin's HEAD, else `main`). In a linked worktree, or on any other branch,
107
+ `openlanes create`, the page's new plan and `resync prs` adoption refuse and
108
+ say why. The same holds for an opening `spec.md` and for `depends_on:`;
109
+ every other file in a plan folder may be written from a worktree. Numbers are handed out under a lock in the shared git directory,
110
+ so two creates at once cannot take the same one.
94
111
 
95
112
  `001.01` is a **sibling of 001 that arrived later, not a part of 001**. The dot
96
113
  reads as containment in almost every other numbering scheme, and here it does
@@ -120,6 +137,53 @@ module OpenLanes
120
137
  MARKDOWN
121
138
  end
122
139
 
140
+ # Where a plan lives, derived from {Plans::Tree::INACTIVE_DIRS}.
141
+ #
142
+ # @return [String]
143
+ def places
144
+ rows = OpenLanes::Plans::Tree::INACTIVE_DIRS.group_by { |_, dir| dir }.map do |dir, pairs|
145
+ states = pairs.map { |key, _| "#{status(key).emoji} #{status(key).label}" }.join(", ")
146
+ "| `#{OpenLanes::PLANS_DIR}/#{dir}/` | #{states} |"
147
+ end
148
+ dated = name("001.00", :approved, "dev-foundation", archived_on: Date.new(2026, 9, 1))
149
+
150
+ <<~MARKDOWN
151
+ ## Where a plan lives
152
+
153
+ The top of `#{OpenLanes::PLANS_DIR}` holds only plans somebody may still work on.
154
+ A plan in one of these states moves into a subfolder, and its name gains the
155
+ date it got there:
156
+
157
+ | Folder | States |
158
+ | --- | --- |
159
+ #{rows.join("\n")}
160
+
161
+ ```
162
+ #{OpenLanes::PLANS_DIR}/#{OpenLanes::Plans::Tree.subfolder_for(status(:approved))}/#{dated}
163
+ ```
164
+
165
+ `openlanes resync dirs` does the moving, with `git mv`, and moves a plan back
166
+ to the top, undated, once its state is open again. The date is the folder's
167
+ last rename in git, else `state.json`'s `updated_at`, else today. Numbers span
168
+ the subfolders: a number in `archive/` is never handed out again.
169
+ `openlanes list-plans` shows the open plans; `--all` adds the rest.
170
+
171
+ While an agent works in a plan, its lock (pid, host, start time) sits in the
172
+ runtime state directory outside the repository (a legacy
173
+ `#{OpenLanes::Plans::Lock::FILENAME}` in the folder is still read), and `list-plans`
174
+ shows 🔑. The folder is never renamed while it is locked. `resync dirs` clears a
175
+ lock whose process is gone from this machine, and only reports one from another.
176
+
177
+ MARKDOWN
178
+ end
179
+
180
+ # @param ordinal [String]
181
+ # @param key [Symbol]
182
+ # @param slug [String]
183
+ # @param archived_on [Date, nil]
184
+ # @return [String] a folder name, spelled the one way {Plans.plan_dirname} spells it
185
+ def name(ordinal, key, slug, archived_on: nil) = OpenLanes::Plans.plan_dirname(ordinal, status(key), slug, archived_on:)
186
+
123
187
  # @return [String]
124
188
  def starting
125
189
  headings = OpenLanes::Lifecycle::Brief::HEADINGS.map { |h| "- `## #{h}`" }.join("\n")
@@ -320,15 +384,22 @@ module OpenLanes
320
384
  | :--- | :---------- |
321
385
  | `spec.md` | the specification; written first |
322
386
  | `plan.md` | the execution plan; written from the spec |
323
- | `implementation-plan.md` | the contract between the two halves of building |
387
+ | `contract.md` | the contract between the two halves of building |
388
+ | `openapi.yaml` | the HTTP surface, when the plan has one |
324
389
  #{known.reject { |r| r.start_with?("| `spec.md`", "| `plan.md`") }.join("\n")}
325
390
 
326
- `implementation-plan.md` is the one file no state requires. `luke-backend`
327
- writes it before it writes code, and `rey-frontend` builds from it a round
328
- later: the interfaces the back end landed, their shapes and their errors,
329
- which files each half owns, which units may run concurrently, and the one
330
- test that proves the halves are joined rather than merely both present. No
331
- state requires it because a plan with no back-end work never needs one.
391
+ `contract.md` is the one file no state requires. `luke-backend` writes it
392
+ before it writes code, and `rey-frontend` builds from it a round later: the
393
+ interfaces the back end landed, their shapes and their errors, which files
394
+ each half owns, which units may run concurrently, and the one test that
395
+ proves the halves are joined rather than merely both present. No state
396
+ requires it because a plan with no back-end work never needs one. When
397
+ `plan.md` says the plan has an HTTP surface, `openapi.yaml` sits beside it,
398
+ linked from it, and `openlanes api docs` lists every one in the project.
399
+
400
+ The units themselves live in `plan.md`, under `## Backend` and
401
+ `## Frontend`. `rey-frontend` is started only when `## Frontend` holds a
402
+ unit heading.
332
403
 
333
404
  Nothing else belongs there. A folder holding notes, diagrams or scratch files
334
405
  is a folder nobody can audit at a glance.
@@ -345,6 +416,28 @@ module OpenLanes
345
416
  .join(", ")
346
417
  end
347
418
 
419
+ # Derived from {Plans::ReviewLabel::ALL}, the only place a label is named.
420
+ #
421
+ # @return [String]
422
+ def review_labels
423
+ rows = OpenLanes::Plans::ReviewLabel::ALL.map do |l|
424
+ "| `#{l.name}` | `#{l.colour}` | #{l.passing? ? "yes" : "no"} | #{l.meaning} |"
425
+ end
426
+
427
+ <<~MARKDOWN
428
+ ## Review labels
429
+
430
+ A pull request carries one of these at a time, set through `openlanes review <pr> <label>`.
431
+ A new label replaces the previous one, except that `#{OpenLanes::Plans::ReviewLabel::VULNERABILITY}`
432
+ sits beside `#{OpenLanes::Plans::ReviewLabel::REJECTED}`. No passing verdict is given while CI is red.
433
+ The engine creates or recolours every one of them on the repository when a run starts.
434
+
435
+ | Label | Colour | Passing | Meaning |
436
+ | --- | --- | --- | --- |
437
+ #{rows.join("\n")}
438
+ MARKDOWN
439
+ end
440
+
348
441
  # @return [String]
349
442
  def pull_requests
350
443
  <<~MARKDOWN
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module Presentation
5
+ # What `resync prs` did, or would do, to pull request titles: one tab-
6
+ # separated row per change for STDOUT, and the retitles, the new plan
7
+ # folders, the skipped pull requests and the assumed-prefix warning for
8
+ # a person on STDERR.
9
+ class PrChanges
10
+ include UI
11
+
12
+ # @param changes [Array<OpenLanes::Lifecycle::Resync::Prs::Change>]
13
+ # @param dry_run [Boolean] whether nothing was actually changed
14
+ def initialize(changes, dry_run:)
15
+ @changes = changes
16
+ @dry_run = dry_run
17
+ end
18
+
19
+ # @return [Array<String>] the deliverable rows: number, new title, reason
20
+ def lines = @changes.map { |change| "#{change.number}\t#{change.new_title || "-"}\t#{change.reason}" }
21
+
22
+ # Draws the report on STDERR.
23
+ #
24
+ # @return [void]
25
+ def report
26
+ applicable, flagged = @changes.partition(&:applicable?)
27
+ assumed = applicable.select(&:assumed?)
28
+
29
+ applicable.each { |change| say("##{change.number} #{change.new_title}#{note(change)}") }
30
+ adoptions(applicable.select(&:adopted?))
31
+ flagged.each { |change| say("##{change.number} #{paint("SKIPPED", :red)} — #{change.reason}", bullet: "!") }
32
+ footer(applicable.size)
33
+ assumptions(assumed)
34
+ end
35
+
36
+ private
37
+
38
+ # @param change [OpenLanes::Lifecycle::Resync::Prs::Change]
39
+ # @return [String, nil]
40
+ def note(change)
41
+ if change.adopted?
42
+ paint(" (new plan)", :magenta)
43
+ elsif change.assumed?
44
+ paint(" (assumed)", :yellow)
45
+ end
46
+ end
47
+
48
+ # Creating folders is a bigger act than editing a title, so it is
49
+ # reported separately rather than buried in the retitle list.
50
+ #
51
+ # @param adopted [Array<OpenLanes::Lifecycle::Resync::Prs::Change>]
52
+ # @return [void]
53
+ def adoptions(adopted)
54
+ return if adopted.empty?
55
+
56
+ verb = @dry_run ? "Would create" : "Created"
57
+ info("#{verb} #{adopted.size} plan folder#{"s" unless adopted.size == 1} for pull " \
58
+ "requests that resolved to no plan:\n\n" \
59
+ "#{adopted.map { |c| " #{c.ordinal} ##{c.number} #{c.title}" }.join("\n")}\n\n" \
60
+ "Each holds its pull request and nothing else. Run `openlanes run`\n" \
61
+ "to have the specification written from what the pull request actually did.")
62
+ end
63
+
64
+ # @param count [Integer] titles retitled, or that would be
65
+ # @return [void]
66
+ def footer(count)
67
+ if @dry_run
68
+ return if count.zero?
69
+
70
+ warn("#{count} retitle#{"s" unless count == 1} pending — nothing was changed.\n" \
71
+ "Re-run without --dry-run to apply.")
72
+ else
73
+ success("Retitled #{count} pull request#{"s" unless count == 1}.")
74
+ end
75
+ end
76
+
77
+ # @param assumed [Array<OpenLanes::Lifecycle::Resync::Prs::Change>]
78
+ # @return [void]
79
+ def assumptions(assumed)
80
+ return if assumed.empty?
81
+
82
+ warn("#{assumed.size} title#{"s" unless assumed.size == 1} would get " \
83
+ "[#{OpenLanes::NO_PLAN_PREFIX}] because no plan resolved.\n\n" \
84
+ "That is an assertion about intent, and it is yours to make: check each one\n" \
85
+ "before committing. A pull request that writes a plan's spec belongs to that\n" \
86
+ "plan however its branch was named.")
87
+ end
88
+ end
89
+ end
90
+ end
@@ -24,8 +24,11 @@ module OpenLanes
24
24
  STATE_WIDTH = Plans::PullRequests::STATES.map { |_, label| UI.display_width(label) }.max
25
25
 
26
26
  # @param tree [OpenLanes::Plans::Tree]
27
- def initialize(tree:)
27
+ # @param waiting_on [Hash{String => Array<Integer>}] plan number to the
28
+ # pull requests open and awaiting a human, from the board snapshot
29
+ def initialize(tree:, waiting_on: {})
28
30
  @tree = tree
31
+ @waiting_on = waiting_on.transform_keys(&:to_s)
29
32
  end
30
33
 
31
34
  # @return [OpenLanes::Plans::Tree]
@@ -54,7 +57,7 @@ module OpenLanes
54
57
  def label_width = @label_width ||= tree.subjects.map { |s| UI.display_width(s.status.label) }.max.to_i + 4
55
58
 
56
59
  # @return [Integer] widest feature name
57
- def title_width = @title_width ||= tree.subjects.map { |s| UI.display_width(s.feature.title) }.max.to_i + 2
60
+ def title_width = @title_width ||= tree.subjects.map { |s| UI.display_width(title_for(s)) }.max.to_i + 2
58
61
 
59
62
  # The one place the table's shape is written down. The heading, the plan
60
63
  # rows, the continuation lines and the violations all come through here, so
@@ -67,7 +70,7 @@ module OpenLanes
67
70
  #
68
71
  # @return [String]
69
72
  def row(flag, ordinal, emoji, label, title, tail)
70
- format("%s %s %s %s %s %s", flag, ordinal, emoji, label, title, tail)
73
+ format("%s %s %s %s %s %s", flag, ordinal, emoji, label, title, tail)
71
74
  end
72
75
 
73
76
  # Cell at which the pull requests start, so a plan's second and third pull
@@ -102,6 +105,25 @@ module OpenLanes
102
105
  [UI.paint(text, :bold), UI.paint(rule, :bright_yellow)].join("\n")
103
106
  end
104
107
 
108
+ # A plan an agent is working on wears 🔑 before its name (a lock held by
109
+ # another machine adds where and since when), and a plan
110
+ # moved aside carries the date it was moved. In the title cell rather
111
+ # than columns of their own, so the pull requests stay where every other
112
+ # row puts them.
113
+ #
114
+ # @param subject [OpenLanes::Plans::Subject]
115
+ # @return [String]
116
+ def title_for(subject)
117
+ feature = subject.feature
118
+ title = feature.archived_on ? "#{feature.title} [#{feature.archived_on.strftime("%F")}]" : feature.title
119
+ lock = Plans::Lock.read(feature.path)
120
+ return title unless lock
121
+
122
+ # A lock from another machine is never cleared, so say whose it is.
123
+ note = lock.foreign? ? " (held on #{lock.host} since #{lock.started_at.utc.iso8601})" : ""
124
+ "🔑 #{title}#{note}"
125
+ end
126
+
105
127
  # One line per plan, plus a continuation line for each extra pull request
106
128
  # so that every URL is visible and clickable rather than truncated.
107
129
  #
@@ -116,13 +138,40 @@ module OpenLanes
116
138
  UI.paint(UI.fit(feature.ordinal.to_s, ORDINAL_WIDTH), :bold),
117
139
  UI.fit(feature.status.emoji, EMOJI_WIDTH),
118
140
  UI.paint(UI.fit(feature.status.label, label_width), :yellow),
119
- UI.paint(UI.fit(feature.title, title_width), :blue, :bold),
120
- prs.empty? ? UI.paint("[none yet]", :bright_red) : link(prs.first)
141
+ UI.paint(UI.fit(title_for(subject), title_width), :blue, :bold),
142
+ [prs.empty? ? UI.paint("[none yet]", :bright_red) : link(prs.first), *blocks_note(feature.ordinal)].join(" ")
121
143
  )
122
144
 
123
145
  rest = prs.drop(1).map { |pr| (" " * pr_column) + link(pr) }
124
146
 
125
- [first, *rest, *violation_line(subject)]
147
+ [first, *rest, *waiting_lines(subject), *violation_line(subject), *dependency_lines(subject)]
148
+ end
149
+
150
+ # What this plan holds up, said in the row's tail so the one line that
151
+ # answers "why is 050 not moving" is on the plan that causes it.
152
+ #
153
+ # @param ordinal [OpenLanes::Plans::Ordinal]
154
+ # @return [Array<String>]
155
+ def blocks_note(ordinal)
156
+ blocked = tree.dependencies.blocks_of(ordinal)
157
+ blocked.empty? ? [] : [UI.paint("blocks #{blocked.join(", ")}", :cyan)]
158
+ end
159
+
160
+ # Dependency trouble under the plan's row. Every answer comes from
161
+ # {Plans::Dependencies}; nothing about the graph is computed here.
162
+ #
163
+ # @param subject [OpenLanes::Plans::Subject]
164
+ # @return [Array<String>]
165
+ def dependency_lines(subject)
166
+ ordinal = subject.feature.ordinal
167
+ deps = tree.dependencies
168
+ notes = Plans::Spec.for(subject).problems.grep(/\Adepends_on/).map { |p| [p, :red] }
169
+ deps.unknown(ordinal).each { |o| notes << ["depends_on: #{o} is not a plan in this tree", :red] }
170
+ deps.closed(ordinal).each { |o| notes << ["depends_on: #{o} is already done, so the entry is ignored", :yellow] }
171
+ cycle = deps.cycle_of(ordinal)
172
+ notes << ["cycle: #{cycle.join(" → ")}", :red] if cycle
173
+
174
+ notes.map { |text, colour| (" " * title_column) + UI.paint("↳ #{text}", colour) }
126
175
  end
127
176
 
128
177
  # The URL is left bare rather than wrapped in an OSC 8 hyperlink: the table
@@ -152,6 +201,16 @@ module OpenLanes
152
201
  end
153
202
  end
154
203
 
204
+ # One line per pull request a human has yet to act on, under the plan.
205
+ #
206
+ # @param subject [OpenLanes::Plans::Subject]
207
+ # @return [Array<String>]
208
+ def waiting_lines(subject)
209
+ Array(@waiting_on[subject.feature.ordinal.to_s]).map do |number|
210
+ (" " * title_column) + UI.paint("↳ waiting on you: PR ##{number}", :bright_yellow)
211
+ end
212
+ end
213
+
155
214
  # @param subject [OpenLanes::Plans::Subject]
156
215
  # @return [Array<String>]
157
216
  def violation_line(subject)
@@ -162,7 +221,7 @@ module OpenLanes
162
221
 
163
222
  # @return [String]
164
223
  def footer
165
- counts = totals.map { |key, n| "#{Plans::STATUS_BY_KEY.fetch(key).emoji} #{n}" }.join(" ")
224
+ counts = totals.map { |key, n| "#{Plans::STATUS_BY_KEY.fetch(key).emoji} #{n}" }.join(" ")
166
225
  lines = [" #{tree.subjects.size} #{tree.subjects.size == 1 ? "plan" : "plans"} #{counts}"]
167
226
 
168
227
  unless inconsistent.empty?
@@ -15,9 +15,28 @@ module OpenLanes
15
15
 
16
16
  # @param tree [OpenLanes::Plans::Tree]
17
17
  # @return [OpenLanes::Presentation::Reporter]
18
- def reporter(tree:) = Reporter.new(tree:)
18
+ def reporter(tree:) = Reporter.new(tree:, waiting_on: waiting_on(tree))
19
+
20
+ # What the engine's watch last saw a person owe, read only while that
21
+ # engine is up: once it stops, nobody is watching, and a pull request
22
+ # merged since would still be listed as waiting.
23
+ #
24
+ # @param tree [OpenLanes::Plans::Tree]
25
+ # @return [Hash{String => Array<Integer>}]
26
+ def waiting_on(tree)
27
+ state = Engine.state_file(tree).load
28
+ state.running? ? state.waiting_on : {}
29
+ end
30
+
31
+ # @param changes [Array<OpenLanes::Lifecycle::Resync::Prs::Change>]
32
+ # @param dry_run [Boolean]
33
+ # @return [OpenLanes::Presentation::PrChanges]
34
+ def pr_changes(changes, dry_run:) = PrChanges.new(changes, dry_run:)
19
35
 
20
36
  # @return [OpenLanes::Presentation::Viewer]
21
37
  def viewer = Viewer.new
38
+
39
+ # @return [OpenLanes::Presentation::ApiDocs] the openapi.yaml files plans carry
40
+ def api_docs = ApiDocs.new
22
41
  end
23
42
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module Setup
5
+ # One question asked of the machine, and its answer.
6
+ #
7
+ # @!attribute name [String] what was checked, as the report labels it
8
+ # @!attribute ok [Boolean]
9
+ # @!attribute detail [String] what was found
10
+ # @!attribute fix [String, nil] the command that turns a ✗ into a ✓
11
+ Check = Data.define(:name, :ok, :detail, :fix) do
12
+ # @return [Boolean]
13
+ def ok? = ok == true
14
+ end
15
+ end
16
+ end