openlanes 3.0.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 (466) hide show
  1. checksums.yaml +7 -0
  2. data/agents/hansolo-reviewer.md +59 -0
  3. data/agents/lando-broker.md +70 -0
  4. data/agents/leah-researcher.md +60 -0
  5. data/agents/luke-backend.md +73 -0
  6. data/agents/palpatine-planner.md +61 -0
  7. data/agents/r2d2-mechanic.md +57 -0
  8. data/agents/rey-frontend.md +68 -0
  9. data/agents/yoda-writer.md +67 -0
  10. data/bin/factory-e2e +157 -0
  11. data/bin/plan-number +164 -0
  12. data/bin/setup +26 -0
  13. data/evals/cases/hansolo-reviewer/review-medium/fail/plan/plan.md +3 -0
  14. data/evals/cases/hansolo-reviewer/review-medium/fail/plan/pull-requests.md +12 -0
  15. data/evals/cases/hansolo-reviewer/review-medium/fail/plan/spec.md +5 -0
  16. data/evals/cases/hansolo-reviewer/review-medium/fail/repo/lib/cart.rb +9 -0
  17. data/evals/cases/hansolo-reviewer/review-medium/fail/repo/spec/cart_spec.rb +7 -0
  18. data/evals/cases/hansolo-reviewer/review-medium/fail/run.json +1 -0
  19. data/evals/cases/hansolo-reviewer/review-medium/pass/plan/plan.md +3 -0
  20. data/evals/cases/hansolo-reviewer/review-medium/pass/plan/pull-requests.md +15 -0
  21. data/evals/cases/hansolo-reviewer/review-medium/pass/plan/spec.md +5 -0
  22. data/evals/cases/hansolo-reviewer/review-medium/pass/repo/lib/cart.rb +9 -0
  23. data/evals/cases/hansolo-reviewer/review-medium/pass/repo/spec/cart_spec.rb +7 -0
  24. data/evals/cases/hansolo-reviewer/review-medium/pass/run.json +1 -0
  25. data/evals/cases/hansolo-reviewer/review-medium.yml +60 -0
  26. data/evals/cases/lando-broker/unblock-fast/fail/plan/blocked.md +9 -0
  27. data/evals/cases/lando-broker/unblock-fast/fail/plan/spec.md +16 -0
  28. data/evals/cases/lando-broker/unblock-fast/fail/run.json +1 -0
  29. data/evals/cases/lando-broker/unblock-fast/pass/plan/spec.md +18 -0
  30. data/evals/cases/lando-broker/unblock-fast/pass/run.json +1 -0
  31. data/evals/cases/lando-broker/unblock-fast.yml +44 -0
  32. data/evals/cases/leah-researcher/research-deep/fail/plan/spec.md +26 -0
  33. data/evals/cases/leah-researcher/research-deep/fail/run.json +1 -0
  34. data/evals/cases/leah-researcher/research-deep/pass/plan/spec.md +39 -0
  35. data/evals/cases/leah-researcher/research-deep/pass/run.json +1 -0
  36. data/evals/cases/leah-researcher/research-deep.yml +51 -0
  37. data/evals/cases/leah-researcher/research-fast/fail/plan/spec.md +27 -0
  38. data/evals/cases/leah-researcher/research-fast/fail/run.json +1 -0
  39. data/evals/cases/leah-researcher/research-fast/pass/plan/spec.md +23 -0
  40. data/evals/cases/leah-researcher/research-fast/pass/run.json +1 -0
  41. data/evals/cases/leah-researcher/research-fast.yml +55 -0
  42. data/evals/cases/luke-backend/build-medium/fail/plan/plan-backend.md +7 -0
  43. data/evals/cases/luke-backend/build-medium/fail/plan/plan-frontend.md +1 -0
  44. data/evals/cases/luke-backend/build-medium/fail/plan/plan.md +3 -0
  45. data/evals/cases/luke-backend/build-medium/fail/plan/pull-requests.md +8 -0
  46. data/evals/cases/luke-backend/build-medium/fail/plan/spec.md +8 -0
  47. data/evals/cases/luke-backend/build-medium/fail/repo/Gemfile +2 -0
  48. data/evals/cases/luke-backend/build-medium/fail/repo/lib/cart.rb +11 -0
  49. data/evals/cases/luke-backend/build-medium/fail/repo/spec/cart_spec.rb +7 -0
  50. data/evals/cases/luke-backend/build-medium/fail/run.json +1 -0
  51. data/evals/cases/luke-backend/build-medium/pass/plan/plan-backend.md +7 -0
  52. data/evals/cases/luke-backend/build-medium/pass/plan/plan-frontend.md +1 -0
  53. data/evals/cases/luke-backend/build-medium/pass/plan/plan.md +3 -0
  54. data/evals/cases/luke-backend/build-medium/pass/plan/pull-requests.md +8 -0
  55. data/evals/cases/luke-backend/build-medium/pass/plan/spec.md +8 -0
  56. data/evals/cases/luke-backend/build-medium/pass/repo/Gemfile +2 -0
  57. data/evals/cases/luke-backend/build-medium/pass/repo/lib/cart.rb +11 -0
  58. data/evals/cases/luke-backend/build-medium/pass/repo/spec/cart_spec.rb +11 -0
  59. data/evals/cases/luke-backend/build-medium/pass/run.json +1 -0
  60. data/evals/cases/luke-backend/build-medium.yml +68 -0
  61. data/evals/cases/palpatine-planner/plan-deep/fail/plan/plan-backend.md +6 -0
  62. data/evals/cases/palpatine-planner/plan-deep/fail/plan/plan.md +8 -0
  63. data/evals/cases/palpatine-planner/plan-deep/fail/plan/spec.md +14 -0
  64. data/evals/cases/palpatine-planner/plan-deep/fail/run.json +1 -0
  65. data/evals/cases/palpatine-planner/plan-deep/pass/plan/plan-backend.md +32 -0
  66. data/evals/cases/palpatine-planner/plan-deep/pass/plan/plan-frontend.md +1 -0
  67. data/evals/cases/palpatine-planner/plan-deep/pass/plan/plan.md +28 -0
  68. data/evals/cases/palpatine-planner/plan-deep/pass/plan/spec.md +14 -0
  69. data/evals/cases/palpatine-planner/plan-deep/pass/run.json +1 -0
  70. data/evals/cases/palpatine-planner/plan-deep.yml +50 -0
  71. data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan-backend.md +48 -0
  72. data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan-frontend.md +1 -0
  73. data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan.md +12 -0
  74. data/evals/cases/palpatine-planner/plan-fast/fail/plan/spec.md +12 -0
  75. data/evals/cases/palpatine-planner/plan-fast/fail/run.json +1 -0
  76. data/evals/cases/palpatine-planner/plan-fast/pass/plan/plan-backend.md +8 -0
  77. data/evals/cases/palpatine-planner/plan-fast/pass/plan/plan-frontend.md +1 -0
  78. data/evals/cases/palpatine-planner/plan-fast/pass/plan/plan.md +12 -0
  79. data/evals/cases/palpatine-planner/plan-fast/pass/plan/spec.md +12 -0
  80. data/evals/cases/palpatine-planner/plan-fast/pass/run.json +1 -0
  81. data/evals/cases/palpatine-planner/plan-fast.yml +47 -0
  82. data/evals/cases/r2d2-mechanic/task-fast/fail/plan/plan.md +7 -0
  83. data/evals/cases/r2d2-mechanic/task-fast/fail/plan/pull-requests.md +6 -0
  84. data/evals/cases/r2d2-mechanic/task-fast/fail/plan/spec.md +6 -0
  85. data/evals/cases/r2d2-mechanic/task-fast/fail/repo/README.md +3 -0
  86. data/evals/cases/r2d2-mechanic/task-fast/fail/repo/lib/greeter/formatter.rb +12 -0
  87. data/evals/cases/r2d2-mechanic/task-fast/fail/repo/lib/greeter.rb +15 -0
  88. data/evals/cases/r2d2-mechanic/task-fast/fail/repo/spec/greeter_spec.rb +7 -0
  89. data/evals/cases/r2d2-mechanic/task-fast/fail/run.json +1 -0
  90. data/evals/cases/r2d2-mechanic/task-fast/pass/plan/plan.md +7 -0
  91. data/evals/cases/r2d2-mechanic/task-fast/pass/plan/pull-requests.md +6 -0
  92. data/evals/cases/r2d2-mechanic/task-fast/pass/plan/spec.md +6 -0
  93. data/evals/cases/r2d2-mechanic/task-fast/pass/repo/README.md +3 -0
  94. data/evals/cases/r2d2-mechanic/task-fast/pass/repo/lib/greeter.rb +3 -0
  95. data/evals/cases/r2d2-mechanic/task-fast/pass/repo/spec/greeter_spec.rb +7 -0
  96. data/evals/cases/r2d2-mechanic/task-fast/pass/run.json +1 -0
  97. data/evals/cases/r2d2-mechanic/task-fast.yml +59 -0
  98. data/evals/cases/r2d2-mechanic/task-medium/fail/plan/plan.md +7 -0
  99. data/evals/cases/r2d2-mechanic/task-medium/fail/plan/pull-requests.md +6 -0
  100. data/evals/cases/r2d2-mechanic/task-medium/fail/plan/spec.md +6 -0
  101. data/evals/cases/r2d2-mechanic/task-medium/fail/repo/Gemfile +2 -0
  102. data/evals/cases/r2d2-mechanic/task-medium/fail/repo/lib/greeter.rb +6 -0
  103. data/evals/cases/r2d2-mechanic/task-medium/fail/repo/spec/greeter_spec.rb +11 -0
  104. data/evals/cases/r2d2-mechanic/task-medium/fail/run.json +1 -0
  105. data/evals/cases/r2d2-mechanic/task-medium/pass/plan/plan.md +7 -0
  106. data/evals/cases/r2d2-mechanic/task-medium/pass/plan/pull-requests.md +6 -0
  107. data/evals/cases/r2d2-mechanic/task-medium/pass/plan/spec.md +6 -0
  108. data/evals/cases/r2d2-mechanic/task-medium/pass/repo/Gemfile +2 -0
  109. data/evals/cases/r2d2-mechanic/task-medium/pass/repo/lib/greeter.rb +6 -0
  110. data/evals/cases/r2d2-mechanic/task-medium/pass/repo/spec/greeter_spec.rb +11 -0
  111. data/evals/cases/r2d2-mechanic/task-medium/pass/run.json +1 -0
  112. data/evals/cases/r2d2-mechanic/task-medium.yml +58 -0
  113. data/evals/cases/rey-frontend/ui-medium/fail/plan/plan-frontend.md +7 -0
  114. data/evals/cases/rey-frontend/ui-medium/fail/plan/plan.md +4 -0
  115. data/evals/cases/rey-frontend/ui-medium/fail/plan/pull-requests.md +8 -0
  116. data/evals/cases/rey-frontend/ui-medium/fail/plan/spec.md +5 -0
  117. data/evals/cases/rey-frontend/ui-medium/fail/repo/lib/cart.rb +5 -0
  118. data/evals/cases/rey-frontend/ui-medium/fail/repo/web/cart_badge.js +4 -0
  119. data/evals/cases/rey-frontend/ui-medium/fail/repo/web/cart_badge.test.js +3 -0
  120. data/evals/cases/rey-frontend/ui-medium/fail/run.json +1 -0
  121. data/evals/cases/rey-frontend/ui-medium/pass/plan/plan-frontend.md +7 -0
  122. data/evals/cases/rey-frontend/ui-medium/pass/plan/plan.md +4 -0
  123. data/evals/cases/rey-frontend/ui-medium/pass/plan/pull-requests.md +8 -0
  124. data/evals/cases/rey-frontend/ui-medium/pass/plan/spec.md +5 -0
  125. data/evals/cases/rey-frontend/ui-medium/pass/repo/lib/cart.rb +3 -0
  126. data/evals/cases/rey-frontend/ui-medium/pass/repo/web/cart_badge.js +4 -0
  127. data/evals/cases/rey-frontend/ui-medium/pass/repo/web/cart_badge.test.js +3 -0
  128. data/evals/cases/rey-frontend/ui-medium/pass/run.json +1 -0
  129. data/evals/cases/rey-frontend/ui-medium.yml +55 -0
  130. data/evals/cases/yoda-writer/spec-medium/fail/plan/plan.md +5 -0
  131. data/evals/cases/yoda-writer/spec-medium/fail/plan/spec.md +45 -0
  132. data/evals/cases/yoda-writer/spec-medium/fail/run.json +1 -0
  133. data/evals/cases/yoda-writer/spec-medium/pass/plan/plan.md +0 -0
  134. data/evals/cases/yoda-writer/spec-medium/pass/plan/spec.md +63 -0
  135. data/evals/cases/yoda-writer/spec-medium/pass/run.json +1 -0
  136. data/evals/cases/yoda-writer/spec-medium.yml +40 -0
  137. data/exe/depot +1 -0
  138. data/exe/lanes +1 -0
  139. data/exe/openlanes +32 -0
  140. data/lib/openlanes/adapters/base.rb +69 -0
  141. data/lib/openlanes/adapters/claude.rb +124 -0
  142. data/lib/openlanes/adapters/codex.rb +33 -0
  143. data/lib/openlanes/adapters/model.rb +37 -0
  144. data/lib/openlanes/adapters/pi.rb +22 -0
  145. data/lib/openlanes/adapters/stream.rb +196 -0
  146. data/lib/openlanes/adapters.rb +92 -0
  147. data/lib/openlanes/agents/agent.rb +103 -0
  148. data/lib/openlanes/agents/profile.rb +57 -0
  149. data/lib/openlanes/agents/registry.rb +151 -0
  150. data/lib/openlanes/agents/roster.rb +103 -0
  151. data/lib/openlanes/agents/routing.rb +88 -0
  152. data/lib/openlanes/agents.rb +20 -0
  153. data/lib/openlanes/cli/agents/subcommands/describe.rb +66 -0
  154. data/lib/openlanes/cli/agents/subcommands/list.rb +20 -0
  155. data/lib/openlanes/cli/base.rb +263 -0
  156. data/lib/openlanes/cli/create/create.rb +354 -0
  157. data/lib/openlanes/cli/docs/docs.rb +40 -0
  158. data/lib/openlanes/cli/eval/eval.rb +110 -0
  159. data/lib/openlanes/cli/index/index.rb +40 -0
  160. data/lib/openlanes/cli/init/init.rb +145 -0
  161. data/lib/openlanes/cli/linear/linear.rb +36 -0
  162. data/lib/openlanes/cli/linear/subcommands/import.rb +174 -0
  163. data/lib/openlanes/cli/linear/subcommands/projects.rb +55 -0
  164. data/lib/openlanes/cli/list_plans/list_plans.rb +21 -0
  165. data/lib/openlanes/cli/mail/mail.rb +31 -0
  166. data/lib/openlanes/cli/mail/subcommands/read.rb +43 -0
  167. data/lib/openlanes/cli/mail/subcommands/send.rb +43 -0
  168. data/lib/openlanes/cli/resync/subcommands/dirs.rb +48 -0
  169. data/lib/openlanes/cli/resync/subcommands/prs.rb +111 -0
  170. data/lib/openlanes/cli/run/run.rb +544 -0
  171. data/lib/openlanes/cli/skills/subcommands/install.rb +92 -0
  172. data/lib/openlanes/cli/start/start.rb +110 -0
  173. data/lib/openlanes/cli/state/state.rb +26 -0
  174. data/lib/openlanes/cli/state/subcommands/show.rb +30 -0
  175. data/lib/openlanes/cli/state/subcommands/sign.rb +48 -0
  176. data/lib/openlanes/cli/states/states.rb +15 -0
  177. data/lib/openlanes/cli/unblock/unblock.rb +235 -0
  178. data/lib/openlanes/cli/version/version.rb +13 -0
  179. data/lib/openlanes/cli/web/web.rb +100 -0
  180. data/lib/openlanes/cli/worktree/worktree.rb +85 -0
  181. data/lib/openlanes/cli.rb +162 -0
  182. data/lib/openlanes/engine/board.rb +104 -0
  183. data/lib/openlanes/engine/dispatcher.rb +1297 -0
  184. data/lib/openlanes/engine/progress_log.rb +125 -0
  185. data/lib/openlanes/engine/runner.rb +224 -0
  186. data/lib/openlanes/engine/state_file.rb +190 -0
  187. data/lib/openlanes/engine/tally.rb +177 -0
  188. data/lib/openlanes/engine.rb +19 -0
  189. data/lib/openlanes/evals/case.rb +191 -0
  190. data/lib/openlanes/evals/changes.rb +67 -0
  191. data/lib/openlanes/evals/check.rb +224 -0
  192. data/lib/openlanes/evals/evidence.rb +71 -0
  193. data/lib/openlanes/evals/live.rb +140 -0
  194. data/lib/openlanes/evals/recording.rb +38 -0
  195. data/lib/openlanes/evals/run.rb +34 -0
  196. data/lib/openlanes/evals/score.rb +24 -0
  197. data/lib/openlanes/evals/scorer.rb +42 -0
  198. data/lib/openlanes/evals/verdict.rb +18 -0
  199. data/lib/openlanes/evals.rb +56 -0
  200. data/lib/openlanes/execution/checks.rb +73 -0
  201. data/lib/openlanes/execution/child.rb +228 -0
  202. data/lib/openlanes/execution/clock.rb +121 -0
  203. data/lib/openlanes/execution/control.rb +194 -0
  204. data/lib/openlanes/execution/executor.rb +849 -0
  205. data/lib/openlanes/execution/prompt.rb +455 -0
  206. data/lib/openlanes/execution/transcript.rb +531 -0
  207. data/lib/openlanes/execution.rb +19 -0
  208. data/lib/openlanes/launcher.rb +69 -0
  209. data/lib/openlanes/lifecycle/adoption.rb +197 -0
  210. data/lib/openlanes/lifecycle/brief.rb +288 -0
  211. data/lib/openlanes/lifecycle/completion.rb +58 -0
  212. data/lib/openlanes/lifecycle/creator.rb +126 -0
  213. data/lib/openlanes/lifecycle/lanes.rb +74 -0
  214. data/lib/openlanes/lifecycle/resync.rb +428 -0
  215. data/lib/openlanes/lifecycle/unblocker.rb +236 -0
  216. data/lib/openlanes/lifecycle.rb +28 -0
  217. data/lib/openlanes/linear/api.rb +224 -0
  218. data/lib/openlanes/linear/attribution.rb +185 -0
  219. data/lib/openlanes/linear/fuzzy.rb +68 -0
  220. data/lib/openlanes/linear/import.rb +314 -0
  221. data/lib/openlanes/linear/issue.rb +187 -0
  222. data/lib/openlanes/linear/mapping.rb +116 -0
  223. data/lib/openlanes/linear/push.rb +197 -0
  224. data/lib/openlanes/linear/survey.rb +174 -0
  225. data/lib/openlanes/linear/unit.rb +274 -0
  226. data/lib/openlanes/linear.rb +41 -0
  227. data/lib/openlanes/live/claims.rb +71 -0
  228. data/lib/openlanes/live/command.rb +122 -0
  229. data/lib/openlanes/live/commands.rb +505 -0
  230. data/lib/openlanes/live/keys.rb +93 -0
  231. data/lib/openlanes/live/memory_store.rb +265 -0
  232. data/lib/openlanes/live/meter.rb +51 -0
  233. data/lib/openlanes/live/node.rb +88 -0
  234. data/lib/openlanes/live/publisher.rb +179 -0
  235. data/lib/openlanes/live/repo.rb +60 -0
  236. data/lib/openlanes/live/run_record.rb +133 -0
  237. data/lib/openlanes/live/run_state.rb +101 -0
  238. data/lib/openlanes/live/session.rb +107 -0
  239. data/lib/openlanes/live/snapshot.rb +322 -0
  240. data/lib/openlanes/live/stations.rb +67 -0
  241. data/lib/openlanes/live/store.rb +277 -0
  242. data/lib/openlanes/live.rb +109 -0
  243. data/lib/openlanes/plans/dev_work.rb +56 -0
  244. data/lib/openlanes/plans/feature.rb +131 -0
  245. data/lib/openlanes/plans/index.rb +208 -0
  246. data/lib/openlanes/plans/ledger.rb +255 -0
  247. data/lib/openlanes/plans/mailbox.rb +164 -0
  248. data/lib/openlanes/plans/ordinal.rb +92 -0
  249. data/lib/openlanes/plans/plan_state.rb +291 -0
  250. data/lib/openlanes/plans/pull_request.rb +29 -0
  251. data/lib/openlanes/plans/pull_requests.rb +229 -0
  252. data/lib/openlanes/plans/spec.rb +213 -0
  253. data/lib/openlanes/plans/state_machine.rb +433 -0
  254. data/lib/openlanes/plans/status.rb +365 -0
  255. data/lib/openlanes/plans/subject.rb +133 -0
  256. data/lib/openlanes/plans/tree.rb +79 -0
  257. data/lib/openlanes/plans.rb +39 -0
  258. data/lib/openlanes/presentation/diagram.rb +149 -0
  259. data/lib/openlanes/presentation/documentation.rb +438 -0
  260. data/lib/openlanes/presentation/reporter.rb +181 -0
  261. data/lib/openlanes/presentation/viewer.rb +62 -0
  262. data/lib/openlanes/presentation.rb +23 -0
  263. data/lib/openlanes/skills/installer.rb +68 -0
  264. data/lib/openlanes/skills/skill.rb +60 -0
  265. data/lib/openlanes/skills.rb +25 -0
  266. data/lib/openlanes/support/config.rb +217 -0
  267. data/lib/openlanes/support/frontmatter.rb +38 -0
  268. data/lib/openlanes/support/markdown.rb +56 -0
  269. data/lib/openlanes/support/project.rb +79 -0
  270. data/lib/openlanes/support/ui.rb +671 -0
  271. data/lib/openlanes/vcs/github.rb +171 -0
  272. data/lib/openlanes/vcs/publisher.rb +221 -0
  273. data/lib/openlanes/vcs/worktree.rb +248 -0
  274. data/lib/openlanes/vcs.rb +19 -0
  275. data/lib/openlanes/version.rb +10 -0
  276. data/lib/openlanes/web/app.rb +176 -0
  277. data/lib/openlanes/web/context.rb +66 -0
  278. data/lib/openlanes/web/demo.rb +169 -0
  279. data/lib/openlanes/web/demo_board.rb +286 -0
  280. data/lib/openlanes/web/events.rb +104 -0
  281. data/lib/openlanes/web/launcher.rb +143 -0
  282. data/lib/openlanes/web/server.rb +77 -0
  283. data/lib/openlanes/web/stores.rb +36 -0
  284. data/lib/openlanes/web.rb +92 -0
  285. data/lib/openlanes.rb +151 -0
  286. data/openlanes.gemspec +87 -0
  287. data/schemas/plan-state.schema.json +101 -0
  288. data/skills/openlanes/SKILL.md +48 -0
  289. data/skills/openlanes-documents/SKILL.md +61 -0
  290. data/skills/openlanes-documents/references/blocked-template.md +49 -0
  291. data/skills/openlanes-documents/references/closing-templates.md +59 -0
  292. data/skills/openlanes-documents/references/plan-template.md +61 -0
  293. data/skills/openlanes-documents/references/retroactive-spec.md +43 -0
  294. data/skills/openlanes-documents/references/spec-template.md +45 -0
  295. data/skills/openlanes-run/SKILL.md +90 -0
  296. data/skills/openlanes-setup/SKILL.md +64 -0
  297. data/skills/openlanes-spec/SKILL.md +90 -0
  298. data/skills/openlanes-status/SKILL.md +32 -0
  299. data/skills/openlanes-sync/SKILL.md +22 -0
  300. data/web/dist/assets/apl-B4CMkyY2.js +2 -0
  301. data/web/dist/assets/asciiarmor-Df11BRmG.js +2 -0
  302. data/web/dist/assets/asn1-EdZsLKOL.js +2 -0
  303. data/web/dist/assets/asterisk-B-8jnY81.js +2 -0
  304. data/web/dist/assets/barlow-condensed-latin-600-normal-BFJEwTuo.woff +0 -0
  305. data/web/dist/assets/barlow-condensed-latin-600-normal-DepVgxBB.woff2 +0 -0
  306. data/web/dist/assets/barlow-condensed-latin-ext-600-normal-18ESti3H.woff2 +0 -0
  307. data/web/dist/assets/barlow-condensed-latin-ext-600-normal-Clv9cIcR.woff +0 -0
  308. data/web/dist/assets/barlow-condensed-vietnamese-600-normal-A5AYRdjN.woff2 +0 -0
  309. data/web/dist/assets/barlow-condensed-vietnamese-600-normal-CNlPk46_.woff +0 -0
  310. data/web/dist/assets/brainfuck-C4LP7Hcl.js +2 -0
  311. data/web/dist/assets/clike-B9uivgTg.js +2 -0
  312. data/web/dist/assets/clojure-BMjYHr_A.js +2 -0
  313. data/web/dist/assets/cmake-BQqOBYOt.js +2 -0
  314. data/web/dist/assets/cobol-CWcv1MsR.js +2 -0
  315. data/web/dist/assets/coffeescript-S37ZYGWr.js +2 -0
  316. data/web/dist/assets/commonlisp-DBKNyK5s.js +2 -0
  317. data/web/dist/assets/crystal-SjHAIU92.js +2 -0
  318. data/web/dist/assets/css-BnMrqG3P.js +2 -0
  319. data/web/dist/assets/cypher-C_CwsFkJ.js +2 -0
  320. data/web/dist/assets/d-pRatUO7H.js +2 -0
  321. data/web/dist/assets/diff-DbItnlRl.js +2 -0
  322. data/web/dist/assets/dockerfile-BKs6k2Af.js +2 -0
  323. data/web/dist/assets/dtd-DF_7sFjM.js +2 -0
  324. data/web/dist/assets/dylan-DwRh75JA.js +2 -0
  325. data/web/dist/assets/ebnf-CDyGwa7X.js +2 -0
  326. data/web/dist/assets/ecl-Cabwm37j.js +2 -0
  327. data/web/dist/assets/eiffel-CnydiIhH.js +2 -0
  328. data/web/dist/assets/elm-vLlmbW-K.js +2 -0
  329. data/web/dist/assets/erlang-BNw1qcRV.js +2 -0
  330. data/web/dist/assets/factor-kuTfRLto.js +2 -0
  331. data/web/dist/assets/favicon-C8pAuqqr.png +0 -0
  332. data/web/dist/assets/fcl-Kvtd6kyn.js +2 -0
  333. data/web/dist/assets/forth-Ffai-XNe.js +2 -0
  334. data/web/dist/assets/fortran-DYz_wnZ1.js +2 -0
  335. data/web/dist/assets/gas-Bneqetm1.js +2 -0
  336. data/web/dist/assets/gherkin-heZmZLOM.js +2 -0
  337. data/web/dist/assets/groovy-D9Dt4D0W.js +2 -0
  338. data/web/dist/assets/haskell-Cw1EW3IL.js +2 -0
  339. data/web/dist/assets/haxe-H-WmDvRZ.js +2 -0
  340. data/web/dist/assets/http-DBlCnlav.js +2 -0
  341. data/web/dist/assets/idl-BEugSyMb.js +2 -0
  342. data/web/dist/assets/index-2xPC-eHt.js +2 -0
  343. data/web/dist/assets/index-8dNhRmiO.js +8 -0
  344. data/web/dist/assets/index-B7-O9ujQ.js +2 -0
  345. data/web/dist/assets/index-BdR34cXC.js +2 -0
  346. data/web/dist/assets/index-Bp7W-ZTq.js +2 -0
  347. data/web/dist/assets/index-Byyxbh_z.js +4 -0
  348. data/web/dist/assets/index-C0VmkrO-.js +2 -0
  349. data/web/dist/assets/index-CI74IkWP.js +198 -0
  350. data/web/dist/assets/index-CYxcbpRM.js +3 -0
  351. data/web/dist/assets/index-D0rmJN3h.js +2 -0
  352. data/web/dist/assets/index-DB4QwuYn.js +2 -0
  353. data/web/dist/assets/index-DDI7zoAS.js +2 -0
  354. data/web/dist/assets/index-DGLUuEXi.js +2 -0
  355. data/web/dist/assets/index-OR-odsl9.js +2 -0
  356. data/web/dist/assets/index-P3vSkiU-.js +2 -0
  357. data/web/dist/assets/index-a4EHR-u4.css +1 -0
  358. data/web/dist/assets/index-mRV4XhJZ.js +2 -0
  359. data/web/dist/assets/index-popk9wbW.js +2 -0
  360. data/web/dist/assets/index-uM88Njdj.js +2 -0
  361. data/web/dist/assets/javascript-iXu5QeM3.js +2 -0
  362. data/web/dist/assets/julia-DuME0IfC.js +2 -0
  363. data/web/dist/assets/livescript-BwQOo05w.js +2 -0
  364. data/web/dist/assets/logo-on-dark-DI9gCdN4.png +0 -0
  365. data/web/dist/assets/logo-on-light-BciwqxcJ.png +0 -0
  366. data/web/dist/assets/lua-VAEuO923.js +2 -0
  367. data/web/dist/assets/mathematica-DTrFuWx2.js +2 -0
  368. data/web/dist/assets/mbox-CNhZ1qSd.js +2 -0
  369. data/web/dist/assets/mirc-CjQqDB4T.js +2 -0
  370. data/web/dist/assets/mllike-CXdrOF99.js +2 -0
  371. data/web/dist/assets/modelica-Dc1JOy9r.js +2 -0
  372. data/web/dist/assets/mscgen-BA5vi2Kp.js +2 -0
  373. data/web/dist/assets/mumps-BT43cFF4.js +2 -0
  374. data/web/dist/assets/nginx-DdIZxoE0.js +2 -0
  375. data/web/dist/assets/nsis-LY0VT_u5.js +2 -0
  376. data/web/dist/assets/ntriples-BfvgReVJ.js +2 -0
  377. data/web/dist/assets/octave-Ck1zUtKM.js +2 -0
  378. data/web/dist/assets/overpass-cyrillic-400-normal-CZsFKZCk.woff +0 -0
  379. data/web/dist/assets/overpass-cyrillic-400-normal-tXDxWGRy.woff2 +0 -0
  380. data/web/dist/assets/overpass-cyrillic-600-normal-B-hc8bdn.woff2 +0 -0
  381. data/web/dist/assets/overpass-cyrillic-600-normal-CXB2Mrig.woff +0 -0
  382. data/web/dist/assets/overpass-cyrillic-800-normal-CP6RWG8X.woff2 +0 -0
  383. data/web/dist/assets/overpass-cyrillic-800-normal-PJW4A9xW.woff +0 -0
  384. data/web/dist/assets/overpass-cyrillic-ext-400-normal-AKUz2ZI5.woff2 +0 -0
  385. data/web/dist/assets/overpass-cyrillic-ext-400-normal-C2ZjKngx.woff +0 -0
  386. data/web/dist/assets/overpass-cyrillic-ext-600-normal-CW4Fknsm.woff2 +0 -0
  387. data/web/dist/assets/overpass-cyrillic-ext-600-normal-Zx-mtRSD.woff +0 -0
  388. data/web/dist/assets/overpass-cyrillic-ext-800-normal-CIth-cA7.woff2 +0 -0
  389. data/web/dist/assets/overpass-cyrillic-ext-800-normal-hx3cL3ZN.woff +0 -0
  390. data/web/dist/assets/overpass-latin-400-normal-BpeLJ0bs.woff2 +0 -0
  391. data/web/dist/assets/overpass-latin-400-normal-c_C8zulu.woff +0 -0
  392. data/web/dist/assets/overpass-latin-600-normal-25RhTNCi.woff2 +0 -0
  393. data/web/dist/assets/overpass-latin-600-normal-DhYQrzaY.woff +0 -0
  394. data/web/dist/assets/overpass-latin-800-normal-Bzx_XP5l.woff2 +0 -0
  395. data/web/dist/assets/overpass-latin-800-normal-D6-q4f0l.woff +0 -0
  396. data/web/dist/assets/overpass-latin-ext-400-normal-BIkXRwl6.woff2 +0 -0
  397. data/web/dist/assets/overpass-latin-ext-400-normal-DUq_h-_z.woff +0 -0
  398. data/web/dist/assets/overpass-latin-ext-600-normal-Cyj4Nio0.woff2 +0 -0
  399. data/web/dist/assets/overpass-latin-ext-600-normal-DQKKMGzp.woff +0 -0
  400. data/web/dist/assets/overpass-latin-ext-800-normal-D6WKWGNs.woff +0 -0
  401. data/web/dist/assets/overpass-latin-ext-800-normal-DZ60AB77.woff2 +0 -0
  402. data/web/dist/assets/overpass-mono-cyrillic-400-normal-B144qALP.woff2 +0 -0
  403. data/web/dist/assets/overpass-mono-cyrillic-400-normal-XK9_jwT8.woff +0 -0
  404. data/web/dist/assets/overpass-mono-cyrillic-ext-400-normal-Dgejvo4A.woff2 +0 -0
  405. data/web/dist/assets/overpass-mono-cyrillic-ext-400-normal-DgxwtUZy.woff +0 -0
  406. data/web/dist/assets/overpass-mono-latin-400-normal-DqAOLzCO.woff +0 -0
  407. data/web/dist/assets/overpass-mono-latin-400-normal-VINZG6Js.woff2 +0 -0
  408. data/web/dist/assets/overpass-mono-latin-ext-400-normal-5YIw-E8g.woff +0 -0
  409. data/web/dist/assets/overpass-mono-latin-ext-400-normal-DBdJzBs_.woff2 +0 -0
  410. data/web/dist/assets/overpass-mono-vietnamese-400-normal-DrLp_cHj.woff +0 -0
  411. data/web/dist/assets/overpass-mono-vietnamese-400-normal-jmOeL0rj.woff2 +0 -0
  412. data/web/dist/assets/overpass-vietnamese-400-normal-BKRCEiXy.woff2 +0 -0
  413. data/web/dist/assets/overpass-vietnamese-400-normal-CCWAWVG7.woff +0 -0
  414. data/web/dist/assets/overpass-vietnamese-600-normal-CjS-4vQf.woff2 +0 -0
  415. data/web/dist/assets/overpass-vietnamese-600-normal-DlMMlPfM.woff +0 -0
  416. data/web/dist/assets/overpass-vietnamese-800-normal-BB8K9-Vo.woff2 +0 -0
  417. data/web/dist/assets/overpass-vietnamese-800-normal-DZUygvj_.woff +0 -0
  418. data/web/dist/assets/oz-BzwKVEFT.js +2 -0
  419. data/web/dist/assets/pascal--L3eBynH.js +2 -0
  420. data/web/dist/assets/perl-CdXCOZ3F.js +2 -0
  421. data/web/dist/assets/pig-CevX1Tat.js +2 -0
  422. data/web/dist/assets/powershell-CFHJl5sT.js +2 -0
  423. data/web/dist/assets/properties-C78fOPTZ.js +2 -0
  424. data/web/dist/assets/protobuf-ChK-085T.js +2 -0
  425. data/web/dist/assets/pug-DeIclll2.js +2 -0
  426. data/web/dist/assets/puppet-DMA9R1ak.js +2 -0
  427. data/web/dist/assets/python-BuPzkPfP.js +2 -0
  428. data/web/dist/assets/q-pXgVlZs6.js +2 -0
  429. data/web/dist/assets/r-B6wPVr8A.js +2 -0
  430. data/web/dist/assets/rpm-CTu-6PCP.js +2 -0
  431. data/web/dist/assets/ruby-BK5aLJev.js +2 -0
  432. data/web/dist/assets/sas-B4kiWyti.js +2 -0
  433. data/web/dist/assets/scheme-C41bIUwD.js +2 -0
  434. data/web/dist/assets/shell-CjFT_Tl9.js +2 -0
  435. data/web/dist/assets/sieve-C3Gn_uJK.js +2 -0
  436. data/web/dist/assets/simple-mode-GW_nhZxv.js +2 -0
  437. data/web/dist/assets/smalltalk-CnHTOXQT.js +2 -0
  438. data/web/dist/assets/solr-DehyRSwq.js +2 -0
  439. data/web/dist/assets/sparql-DkYu6x3z.js +2 -0
  440. data/web/dist/assets/spreadsheet-BCZA_wO0.js +2 -0
  441. data/web/dist/assets/sql-D0XecflT.js +2 -0
  442. data/web/dist/assets/stex-C3f8Ysf7.js +2 -0
  443. data/web/dist/assets/stylus-B533Al4x.js +2 -0
  444. data/web/dist/assets/swift-BzpIVaGY.js +2 -0
  445. data/web/dist/assets/tcl-DVfN8rqt.js +2 -0
  446. data/web/dist/assets/textile-CnDTJFAw.js +2 -0
  447. data/web/dist/assets/tiddlywiki-DO-Gjzrf.js +2 -0
  448. data/web/dist/assets/tiki-DGYXhP31.js +2 -0
  449. data/web/dist/assets/toml-Bm5Em-hy.js +2 -0
  450. data/web/dist/assets/troff-wAsdV37c.js +2 -0
  451. data/web/dist/assets/ttcn-CfJYG6tj.js +2 -0
  452. data/web/dist/assets/ttcn-cfg-B9xdYoR4.js +2 -0
  453. data/web/dist/assets/turtle-B1tBg_DP.js +2 -0
  454. data/web/dist/assets/vb-CmGdzxic.js +2 -0
  455. data/web/dist/assets/vbscript-BuJXcnF6.js +2 -0
  456. data/web/dist/assets/velocity-D8B20fx6.js +2 -0
  457. data/web/dist/assets/verilog-C6RDOZhf.js +2 -0
  458. data/web/dist/assets/vhdl-lSbBsy5d.js +2 -0
  459. data/web/dist/assets/webidl-ZXfAyPTL.js +2 -0
  460. data/web/dist/assets/xquery-DzFWVndE.js +2 -0
  461. data/web/dist/assets/yacas-BJ4BC0dw.js +2 -0
  462. data/web/dist/assets/z80-Hz9HOZM7.js +2 -0
  463. data/web/dist/index.html +14 -0
  464. data/web/dist/vite.svg +1 -0
  465. data/web/src/api/fixtures/board.json +512 -0
  466. metadata +907 -0
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module CLI
5
+ # `openlanes start` — the factory as a long-lived service: the engine and
6
+ # the control room together, with or without any plans.
7
+ #
8
+ # It keeps the board published and the commands read, rescans the tree,
9
+ # and starts agents as soon as an answer, a new plan or a hand edit makes
10
+ # something dispatchable. It never ends on its own, not even once every
11
+ # plan is finished: the page creates the next plan. It ends on Stop All
12
+ # Local in the page, Ctrl-C or SIGTERM.
13
+ #
14
+ # The page is the only way in: every decision a person makes goes through
15
+ # it, and the terminal is output only. So there is no headless fallback
16
+ # (Redis and the control room are required, not hoped for), no dry run,
17
+ # no `--no-web` and no `--until-settled`.
18
+ class Start < Run
19
+ desc "web + run in one: the engine and its control room, with or without plans, until Stop All Local"
20
+
21
+ # Inherited from `run`, and meaningless here: a dry run publishes no
22
+ # board, and a run without its page cannot be answered.
23
+ @options = options.reject { %i[dry_run web until_settled].include?(it.name) }
24
+
25
+ option :rescan,
26
+ default: Engine::Dispatcher::RESCAN.to_s,
27
+ desc: "Seconds between rescans of .plans while idle; a command from the page rescans at once"
28
+
29
+ example [
30
+ " # engine and control room, until Stop All Local or Ctrl-C",
31
+ "--no-open # print the control room's URL, open no browser",
32
+ "--port 4848 -j 4 # somewhere other than 11000, four agents at a time",
33
+ "--plan 005 # only plan 005, until stopped"
34
+ ]
35
+
36
+ # Exit status when Redis or the control room is not there to serve.
37
+ UNAVAILABLE = 69
38
+
39
+ # The progress log, echoed to the terminal: the terminal is where a
40
+ # person watches `start` from when the page is closed, and it takes
41
+ # no input.
42
+ #
43
+ # @param options [Hash]
44
+ # @return [void]
45
+ def call(**options)
46
+ @rescan = Integer(options.fetch(:rescan, Engine::Dispatcher::RESCAN), exception: false)
47
+ refuse("--rescan takes a whole number of seconds, at least 1.", 64) unless @rescan&.positive?
48
+
49
+ tap = UI.tap_log(lambda { |message, fields|
50
+ next if UI.quiet
51
+
52
+ Kernel.warn(Engine::ProgressLog.render(message, **fields.slice(:plan, :status, :agent, :seconds, :round)))
53
+ })
54
+ super
55
+ ensure
56
+ UI.untap_log(tap) if tap
57
+ end
58
+
59
+ private
60
+
61
+ # @param _options [Hash]
62
+ # @param _web [OpenLanes::Web::Launcher]
63
+ # @return [Hash]
64
+ def staying(_options, _web) = { stay: true, rescan: @rescan }
65
+
66
+ # @return [void]
67
+ def without_live(message, _options)
68
+ refuse("#{message}\n\nopenlanes start needs Redis: its control room is the only way to answer the " \
69
+ "agents' questions. Start redis-server, or pass --redis-url. " \
70
+ "`openlanes run` goes on without it, headless.",
71
+ UNAVAILABLE)
72
+ end
73
+
74
+ # @return [void]
75
+ def without_web(web, live, _options)
76
+ live.close
77
+ refuse("The control room did not answer on #{web.url}, and openlanes start is no use without it. " \
78
+ "Its log: #{web.log}",
79
+ UNAVAILABLE)
80
+ end
81
+
82
+ # A project with nothing left to do opens on the idea dialog (W19).
83
+ #
84
+ # @return [String]
85
+ def browser_url(web, tree)
86
+ open = tree.subjects.reject { Engine::Dispatcher::FINISHED.include?(it.status.key) }
87
+ open.empty? ? "#{web.url}#start" : web.url
88
+ end
89
+
90
+ # What the service did, and exit 0: stopping it on purpose is not a
91
+ # failure, and the failed attempts it lived through are listed above
92
+ # the box.
93
+ #
94
+ # @return [void]
95
+ def finish(runner, attempts, options, seconds:)
96
+ puts ""
97
+ puts attempt_lines(attempts)
98
+ puts("", Engine.tally(attempts:, seconds:).render)
99
+ unless quiet?(options)
100
+ blocked = runner.blocked.map { it.feature.ordinal }
101
+ lines = ["Stopped. Unfinished plans keep their state; openlanes start picks them up again."]
102
+ lines << "Still waiting for answers: #{blocked.join(", ")}" unless blocked.empty?
103
+ success("#{attempts.size} invocation#{"s" unless attempts.size == 1}, " \
104
+ "#{attempts.count(&:advanced?)} advanced\n\n#{lines.join("\n")}")
105
+ end
106
+ exit(0)
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module CLI
5
+ # `openlanes state` — a plan's `state.json`, read and signed. Agents sign
6
+ # through this rather than editing the file, because the command takes the
7
+ # lock that keeps two agents on one plan from overwriting each other.
8
+ module State
9
+ # Both subcommands name a plan and need its folder.
10
+ module Locating
11
+ private
12
+
13
+ # @param options [Hash]
14
+ # @return [OpenLanes::Plans::PlanState]
15
+ def plan_state_for(options)
16
+ tree = tree_for(options)
17
+ ordinal = Plans::Ordinal.parse(options[:plan].to_s)
18
+ subject = ordinal && tree.find(ordinal)
19
+ refuse("No plan #{options[:plan]} in #{tree.dir}.\n\nKnown: #{tree.ordinals.join(", ")}", 66) unless subject
20
+
21
+ Plans::PlanState.for(subject.feature.path)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module OpenLanes
6
+ module CLI
7
+ module State
8
+ # `openlanes state show` — the plan's `state.json`, on STDOUT, so it
9
+ # pipes into `jq`.
10
+ class Show < Base
11
+ include Locating
12
+
13
+ desc "Print a plan's state.json"
14
+
15
+ option :plan, required: true, desc: "Which plan: NNN or NNN.MM"
16
+
17
+ example [
18
+ "--plan 003",
19
+ "--plan 003 | jq '.stages[] | {agent, round, status, model, seconds}'"
20
+ ]
21
+
22
+ # @param options [Hash]
23
+ # @return [void]
24
+ def call(**options)
25
+ $stdout.puts(JSON.pretty_generate(plan_state_for(options).read))
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module CLI
5
+ module State
6
+ # `openlanes state sign` — an agent's ledger line, written into the
7
+ # plan's `state.json`.
8
+ class Sign < Base
9
+ include Locating
10
+
11
+ desc "Sign a plan's ledger: Started, Completed, Almost completed, Interrupted or Blocked"
12
+
13
+ option :plan, required: true, desc: "Which plan: NNN or NNN.MM"
14
+ option :agent, required: true, desc: "The agent signing"
15
+ option :round, required: true, desc: "Which round this is, as the prompt states it"
16
+ option :status, required: true, values: Plans::Ledger::STATUSES, desc: "Where the agent stands"
17
+ option :note, desc: "A word or two the harness reads, e.g. approved, rejected, technical, product"
18
+ option :next, desc: "Only with Completed: the agent that should go next"
19
+
20
+ example [
21
+ "--plan 003 --agent luke-backend --round 1 --status Started",
22
+ "--plan 003 --agent hansolo-reviewer --round 1 --status Completed --note approved",
23
+ "--plan 003 --agent yoda-writer --round 1 --status Completed --next palpatine-planner"
24
+ ]
25
+
26
+ # @param options [Hash]
27
+ # @return [void]
28
+ def call(**options)
29
+ state = plan_state_for(options)
30
+ signature = begin
31
+ state.sign!(agent: options[:agent],
32
+ round: Integer(options[:round].to_s, exception: false) || refuse("--round must be a number", 64),
33
+ status: options[:status],
34
+ note: options[:note],
35
+ next_agent: options[:next])
36
+ rescue OpenLanes::Error => e
37
+ refuse(e.message, 64)
38
+ end
39
+
40
+ return if quiet?(options)
41
+
42
+ UI.line("#{signature["agent"]}: #{signature["status"]}, round #{signature["round"]} " \
43
+ "in #{File.basename(state.dir)}/#{Plans::PlanState::FILENAME}")
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module CLI
5
+ # `openlanes states` — the state machine, as a terminal diagram.
6
+ class States < Dry::CLI::Command
7
+ desc "Print a diagram of every state and the transitions between them"
8
+
9
+ example [""]
10
+
11
+ # @return [void]
12
+ def call(**) = $stdout.write(Presentation.diagram)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module CLI
5
+ # `openlanes unblock NNN…` — hand a stopped plan to the agent that
6
+ # drains its `blocked.md`.
7
+ #
8
+ # Deliberately not part of `run`. ⭕️ and 🅱️ are {StateMachine::SETTLED}, so
9
+ # the loop never offers a blocked plan to anybody, and that is the property
10
+ # that makes the state mean anything: the plan waits for a human, and no
11
+ # agent quietly decides otherwise. Answers arriving is not a fact the tool
12
+ # can observe, so a human typing this command *is* the signal, and there is
13
+ # nothing else that could produce it.
14
+ class Unblock < Base
15
+ # The agent that knows the shape of `blocked.md`.
16
+ DEFAULT_AGENT = "lando-broker"
17
+
18
+ # What each token turned out to be, for whoever has to fix it.
19
+ PROBLEMS = {
20
+ missing: "no plan of that number",
21
+ not_blocked: "no blocked.md, so there is nothing to drain"
22
+ }.freeze
23
+
24
+ desc "Fold answered blocks into a plan's documents and retire blocked.md"
25
+
26
+ argument :plans,
27
+ type: :array,
28
+ required: true,
29
+ desc: "Which plans to drain: NNN or NNN.MM, e.g. 003 005.01"
30
+
31
+ dry_run_option "Invoke no agent; print the questions still open"
32
+ option :agent, default: DEFAULT_AGENT, desc: "Hand the folder to a different agent"
33
+ option :root, desc: "Repository root the agent works in (default: the .plans parent)"
34
+
35
+ example [
36
+ "003 -n # what 003 is still waiting on",
37
+ "003 # fold in whatever has been answered",
38
+ "003,005 # both"
39
+ ]
40
+
41
+ # @param plans [Array<String>]
42
+ # @param options [Hash]
43
+ # @return [void]
44
+ def call(plans:, **options)
45
+ tree = tree_for(options)
46
+ quiet?(options)
47
+ unblocker = Lifecycle.unblocker(tree:,
48
+ agent: agent_for(options),
49
+ root: options[:root],
50
+ commit: !dry_run?(options),
51
+ executor: Execution.executor(root: options[:root] || File.dirname(tree.dir),
52
+ dry_run: dry_run?(options)))
53
+
54
+ targets = unblocker.resolve(plans)
55
+ refused(targets, tree, options)
56
+ subjects = targets.select(&:drainable?).map(&:subject)
57
+ exit(worst(targets, [])) if subjects.empty?
58
+
59
+ credentials_warning if !dry_run?(options) && !quiet?(options)
60
+ preflight(subjects, unblocker, options)
61
+
62
+ outcomes = Execution.on_interrupt { unblocker.call(subjects) }
63
+ outcomes.each { |outcome| report(outcome, options) }
64
+ footer(outcomes, options) unless quiet?(options)
65
+ exit(worst(targets, outcomes))
66
+ end
67
+
68
+ private
69
+
70
+ # @param options [Hash]
71
+ # @return [OpenLanes::Agents::Agent]
72
+ def agent_for(options)
73
+ name = options.fetch(:agent, DEFAULT_AGENT)
74
+ agents = OpenLanes::Agents.registry
75
+ agents.find(name) or
76
+ refuse("No agent called #{name}.\n\nKnown: #{agents.all.map(&:name).join(", ")}", 65)
77
+ end
78
+
79
+ # A number that names no folder, or a folder that was never stopped, is
80
+ # refused rather than skipped. Whoever typed this believes an answer has
81
+ # arrived; running against a folder nobody blocked and saying nothing is
82
+ # how you come back later to a plan nobody touched and no record of why.
83
+ #
84
+ # Every bad token is reported, not just the first, because stopping at
85
+ # the first one hides the rest of the answer.
86
+ #
87
+ # @param targets [Array<OpenLanes::Lifecycle::Unblocker::Target>]
88
+ # @param tree [OpenLanes::Plans::Tree]
89
+ # @param options [Hash]
90
+ # @return [void]
91
+ def refused(targets, tree, options)
92
+ problems = targets.reject(&:drainable?)
93
+ return if problems.empty?
94
+
95
+ problems.each do |target|
96
+ state = target.subject ? target.subject.status.key : "unknown"
97
+ puts "#{target.token}\t#{state}\t0 open\t#{PROBLEMS.fetch(target.problem)}"
98
+ end
99
+ return if quiet?(options)
100
+
101
+ error("#{problems.size} of the plans named cannot be drained:\n\n" +
102
+ problems.map { |t| " #{t.token} — #{PROBLEMS.fetch(t.problem)}" }.join("\n") +
103
+ "\n\nThe tree holds: #{tree.ordinals.join(", ")}")
104
+ end
105
+
106
+ # What each folder is waiting on, said BEFORE the agent is invoked.
107
+ #
108
+ # This is the half that was missing. A real run hands the folder to an
109
+ # agent that can take a quarter of an hour, and until it came back the
110
+ # terminal showed nothing at all — a run that had found nothing to do and
111
+ # a run still working looked exactly alike.
112
+ #
113
+ # @param subjects [Array<OpenLanes::Plans::Subject>]
114
+ # @param unblocker [OpenLanes::Lifecycle::Unblocker]
115
+ # @param options [Hash]
116
+ # @return [void]
117
+ def preflight(subjects, _unblocker, options)
118
+ return if quiet?(options)
119
+
120
+ subjects.each do |subject|
121
+ questions = Lifecycle::Unblocker.questions(subject)
122
+ answered = questions.count(&:answered)
123
+ say("#{paint(subject.feature.ordinal.to_s, :bright_black)} #{subject.status.emoji} " \
124
+ "#{subject.feature.title} — #{summary(questions.size, answered)}")
125
+ detail(subject, questions)
126
+ end
127
+ end
128
+
129
+ # @param open [Integer]
130
+ # @param answered [Integer]
131
+ # @return [String]
132
+ def summary(open, answered)
133
+ return "nothing open" if open.zero?
134
+
135
+ "#{open} open, #{answered.zero? ? "none answered yet" : "#{answered} with an answer waiting"}"
136
+ end
137
+
138
+ # @param subject [OpenLanes::Plans::Subject]
139
+ # @param questions [Array<OpenLanes::Lifecycle::Unblocker::Question>]
140
+ # @return [void]
141
+ def detail(subject, questions)
142
+ if subject.unreadable_block?
143
+ say(" #{paint("blocked.md names no `## B<n>` question, so nothing here can be drained", :red)}",
144
+ bullet: " ")
145
+ say(" #{paint("Number each open question `## B1`, `## B2`, and each answer `## A1`, `## A2`.", :yellow)}",
146
+ bullet: " ")
147
+ elsif questions.empty?
148
+ say(" #{paint("nothing left open", :green)}", bullet: " ")
149
+ else
150
+ questions.each { |question| say(" #{paint(question.to_s, :yellow)}", bullet: " ") }
151
+ end
152
+ end
153
+
154
+ # One plan, after the fact: the deliverable line on STDOUT, and what
155
+ # changed on STDERR.
156
+ #
157
+ # @param outcome [OpenLanes::Lifecycle::Unblocker::Outcome]
158
+ # @param options [Hash]
159
+ # @return [void]
160
+ def report(outcome, options)
161
+ moved = movement(outcome, !dry_run?(options))
162
+ puts "#{outcome.ordinal}\t#{outcome.ok ? outcome.subject.status.key : "failed"}\t" \
163
+ "#{outcome.after.size} open\t#{moved}\t#{outcome.note}"
164
+ # A dry run changed nothing, so the preflight above it is still the
165
+ # whole truth. Printing the same three lines again reads as a second
166
+ # pass that found the same thing, which is not what happened.
167
+ return if quiet?(options) || dry_run?(options)
168
+
169
+ say("#{paint(outcome.ordinal.to_s, :bright_black)} #{outcome.subject.status.emoji} " \
170
+ "#{outcome.subject.feature.title} — #{paint(moved, outcome.ok ? :green : :red)}")
171
+ detail(outcome.subject, outcome.after)
172
+ end
173
+
174
+ # What the file says happened, rather than what the agent claims. The
175
+ # questions are counted off disk on both sides of the run.
176
+ #
177
+ # @param outcome [OpenLanes::Lifecycle::Unblocker::Outcome]
178
+ # @param commit [Boolean]
179
+ # @return [String]
180
+ def movement(outcome, commit)
181
+ return "failed" unless outcome.ok
182
+ return "not attempted" unless commit
183
+ return "blocked.md retired" if outcome.cleared?
184
+ return "nothing folded" if outcome.folded.empty?
185
+
186
+ "folded #{outcome.folded.map { |n| "B#{n}" }.join(", ")}"
187
+ end
188
+
189
+ # @param outcomes [Array<OpenLanes::Lifecycle::Unblocker::Outcome>]
190
+ # @param options [Hash]
191
+ # @return [void]
192
+ def footer(outcomes, options)
193
+ failed = outcomes.reject(&:ok)
194
+
195
+ if dry_run?(options)
196
+ warn("Dry run: no agent was invoked, and #{outcomes.size} " \
197
+ "plan#{"s" unless outcomes.size == 1} #{outcomes.size == 1 ? "is" : "are"} unchanged.\n" \
198
+ "Re-run without --dry-run to fold in whatever has been answered.")
199
+ return
200
+ end
201
+
202
+ return error(failed.map { |o| "#{o.ordinal}: #{o.note}" }.join("\n")) unless failed.empty?
203
+
204
+ cleared = outcomes.count(&:cleared?)
205
+ folded = outcomes.sum { |o| o.folded.size }
206
+ waiting = outcomes.sum { |o| o.after.size }
207
+ success("#{folded} question#{"s" unless folded == 1} folded in · " \
208
+ "#{cleared} plan#{"s" unless cleared == 1} out of the block · " \
209
+ "#{waiting} still waiting on a human." +
210
+ (if folded.zero?
211
+ "\n\nNothing moved. An answer has to be written into blocked.md as its own " \
212
+ "`## A<n>` section, answering the `## B<n>` of the same number, before " \
213
+ "there is anything to fold."
214
+ else
215
+ ""
216
+ end))
217
+ end
218
+
219
+ # The most serious thing that happened, as an exit status. A tree that
220
+ # was partly drained still exits non-zero when part of it could not be.
221
+ #
222
+ # @param targets [Array<OpenLanes::Lifecycle::Unblocker::Target>]
223
+ # @param outcomes [Array<OpenLanes::Lifecycle::Unblocker::Outcome>]
224
+ # @return [Integer]
225
+ def worst(targets, outcomes)
226
+ problems = targets.map(&:problem)
227
+ return 66 if problems.include?(:missing)
228
+ return 65 if problems.include?(:not_blocked)
229
+ return 1 unless outcomes.all?(&:ok)
230
+
231
+ 0
232
+ end
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module CLI
5
+ # `openlanes version`
6
+ class Version < Dry::CLI::Command
7
+ desc "Print the version and exit"
8
+
9
+ # @return [void]
10
+ def call(**) = puts("openlanes #{OpenLanes::VERSION}")
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module CLI
5
+ # `openlanes web` — the control room's API server on its own. `openlanes run`
6
+ # starts one beside the engine; this is that server, and with `--demo`
7
+ # the same server over a pretend engine, for Cypress and front-end work.
8
+ class Web < Base
9
+ desc "Serve the control room page and its API on 127.0.0.1"
10
+
11
+ option :port,
12
+ default: OpenLanes::Web::DEFAULT_PORT.to_s,
13
+ desc: "Port to listen on"
14
+ option :redis_url,
15
+ desc: "Redis the engine publishes to (default: $OPENLANES_REDIS_URL, else .openlanes.yml redis_url, else $REDIS_URL, " \
16
+ "else #{Live::DEFAULT_URL})"
17
+ option :open,
18
+ type: :boolean,
19
+ default: true,
20
+ desc: "Open the page in the browser; --no-open prints the URL only"
21
+ option :demo,
22
+ type: :boolean,
23
+ default: false,
24
+ desc: "Serve a sample run that moves and answers commands, with no agents. " \
25
+ "Uses Redis when it answers, an in-memory store when it does not"
26
+
27
+ example [
28
+ " # serve the live run of this checkout",
29
+ "--demo --no-open # the sample run, for Cypress and npm run dev",
30
+ "--port 4848 # somewhere other than 11000"
31
+ ]
32
+
33
+ # @param options [Hash]
34
+ # @return [void]
35
+ def call(**options)
36
+ tree = tree_for(options)
37
+ root = File.dirname(tree.dir)
38
+ url = Live.redis_url(option: redis_url_option(options), config: Config.current)
39
+ stores = stores_for(url, options)
40
+ node = Live::Node.current(root:)
41
+ keys = Live.keys(root:, node:)
42
+ keys = keys.with(repo: "demo/#{keys.repo}") if options[:demo]
43
+ demo = OpenLanes::Web.demo(store: stores.current, keys:).start if options[:demo]
44
+ context = OpenLanes::Web.context(stores:, keys:, plans_dir: tree.dir, url: stores.current.url, node:, demo:)
45
+ server = OpenLanes::Web.server(app: OpenLanes::Web.app(context), context:, port: options[:port])
46
+ serve(server, options, keys:, node:, url: stores.current.url)
47
+ ensure
48
+ demo&.stop
49
+ end
50
+
51
+ private
52
+
53
+ # `--redis-url`, else the URL `openlanes run` hands its server through the
54
+ # environment, so a password never shows in `ps`. Both are passed to
55
+ # {Live.redis_url} as the option, which keeps the rest of its order.
56
+ #
57
+ # @param options [Hash]
58
+ # @param env [Hash]
59
+ # @return [String, nil]
60
+ def redis_url_option(options, env: ENV)
61
+ [options[:redis_url], env[OpenLanes::Web::Launcher::REDIS_URL_ENV]].find { !it.to_s.strip.empty? }
62
+ end
63
+
64
+ # @return [OpenLanes::Web::Stores]
65
+ def stores_for(url, options)
66
+ store = Live.connect(url)
67
+ store.close
68
+ OpenLanes::Web::Stores.new { Live.store(url) }
69
+ rescue Live::Unavailable => e
70
+ unless options[:demo]
71
+ refuse("#{e.message}\n\nThe page reads the run from Redis. Start redis-server, pass --redis-url, " \
72
+ "or try --demo.",
73
+ 69)
74
+ end
75
+ warn("#{e.message}\n\nThe demo keeps its board in memory instead: this page only, no other machine.") unless quiet?(options)
76
+ OpenLanes::Web::Stores.shared(Live.memory_store)
77
+ end
78
+
79
+ # Listen until INT or TERM.
80
+ #
81
+ # @return [void]
82
+ def serve(server, options, keys:, node:, url:)
83
+ begin
84
+ server.start
85
+ rescue Errno::EADDRINUSE
86
+ refuse("Port #{options[:port]} is taken: another `openlanes web` may already be serving. " \
87
+ "Pass --port to use another.",
88
+ 69)
89
+ end
90
+ %w[INT TERM].each { |name| Signal.trap(name) { Thread.new { server.stop } } }
91
+ unless quiet?(options)
92
+ info("Control room: #{server.url}#{" (demo)" if options[:demo]}\n" \
93
+ "Redis: #{Live.mask(url)} as #{node.label} (#{keys.node}), repository #{keys.repo}")
94
+ end
95
+ OpenLanes::Web.open(server.url) if options[:open]
96
+ server.wait
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module CLI
5
+ # `openlanes worktree` — create and seed worktrees for plans.
6
+ class Worktree < Base
7
+ desc "Create and seed a worktree for a plan"
8
+
9
+ argument :plan,
10
+ required: true,
11
+ desc: "Plan ordinal (e.g. 002, 002.00, or a plan folder name)"
12
+
13
+ option :skip_seed,
14
+ type: :boolean,
15
+ default: false,
16
+ desc: "Create the worktree but skip seeding it with .env and credential files"
17
+
18
+ example [
19
+ "002 # create/seed the worktree for plan 002.00",
20
+ "002.01 # create/seed the worktree for plan 002.01",
21
+ "002.00-⚪️ → update-dsl # find the plan by its folder name",
22
+ "--skip-seed 003 # create the worktree but don't seed it"
23
+ ]
24
+
25
+ # @param plan [String] plan identifier
26
+ # @param options [Hash]
27
+ # @return [void]
28
+ def call(plan:, **options)
29
+ tree = tree_for(options)
30
+ is_quiet = quiet?(options)
31
+
32
+ feature = resolve_plan(tree, plan)
33
+ unless feature
34
+ refuse("Could not find plan: #{plan}", 66)
35
+ end
36
+
37
+ root = File.dirname(tree.dir)
38
+ worktree = OpenLanes::Vcs.worktree(root:)
39
+
40
+ checkout = worktree.checkout_for(feature)
41
+
42
+ return if is_quiet
43
+
44
+ if checkout.created?
45
+ puts("✓ Created worktree for #{feature.ordinal}: #{checkout.path}")
46
+ else
47
+ puts("✓ Using existing worktree for #{feature.ordinal}: #{checkout.path}")
48
+ end
49
+
50
+ unless options[:skip_seed]
51
+ seeded = worktree.seed(checkout.path)
52
+ if seeded
53
+ puts("✓ Seeded with .env and credential files")
54
+ else
55
+ warn("⚠ Could not seed worktree, see above for details")
56
+ end
57
+ end
58
+
59
+ puts("")
60
+ success("Worktree ready for #{feature.ordinal}")
61
+ puts("Path: #{checkout.path}")
62
+ puts("Branch: #{checkout.branch}")
63
+ end
64
+
65
+ private
66
+
67
+ # Find the plan that matches the given identifier.
68
+ #
69
+ # @param tree [OpenLanes::Plans::Tree] plans directory wrapper
70
+ # @param plan [String] ordinal, folder name, or slug
71
+ # @return [OpenLanes::Plans::Feature, nil]
72
+ def resolve_plan(tree, plan)
73
+ # Try parsing as an ordinal first
74
+ ordinal = ::OpenLanes::Plans::Ordinal.parse(plan)
75
+ if ordinal
76
+ found = tree.features.find { |f| f.ordinal == ordinal }
77
+ return found if found
78
+ end
79
+
80
+ # Try matching by folder name
81
+ tree.features.find { |f| f.dirname == plan }
82
+ end
83
+ end
84
+ end
85
+ end