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 eval` — score the agents against their cases.
6
+ #
7
+ # Offline by default: every case's recorded folder is scored, which costs
8
+ # nothing and runs in CI. `--live` runs the real agents and is never the
9
+ # default, because it spends tokens.
10
+ class Eval < Base
11
+ standalone!
12
+ desc "Score agents against evals/cases: recorded folders by default, real runs with --live"
13
+
14
+ option :agent, aliases: ["-a"], desc: "Only this agent's cases"
15
+ option :case, aliases: ["-c"], desc: "Only the case with this id"
16
+ option :depth,
17
+ values: Plans::Spec::DEPTHS.map(&:to_s),
18
+ desc: "Hold every case to this depth's bounds instead of its own"
19
+ option :recording,
20
+ values: %w[pass fail],
21
+ default: "pass",
22
+ desc: "Which recording to score offline"
23
+ option :live,
24
+ type: :boolean,
25
+ default: false,
26
+ desc: "Run the real agent in a temp repository (spends tokens)"
27
+ option :max_tokens,
28
+ type: :integer,
29
+ desc: "Token cap per live case (default #{Evals::Live::DEFAULT_MAX_TOKENS})"
30
+ option :cases, desc: "Directory of <agent>/<id>.yml cases (default: the gem's evals/cases)"
31
+
32
+ example [
33
+ " # score every pass/ recording",
34
+ "--recording fail # the fail/ ones, which should all FAIL",
35
+ "-a leah-researcher --depth deep",
36
+ "--live -c research-fast --max-tokens 100000"
37
+ ]
38
+
39
+ # @param options [Hash]
40
+ # @return [void]
41
+ def call(**options)
42
+ quiet?(options)
43
+ refuse("--max-tokens only applies to --live runs.", 64) if options[:max_tokens] && !options[:live]
44
+ cases = selected(options)
45
+ scores = options[:live] ? live(cases, options) : offline(cases, options.fetch(:recording, "pass"))
46
+ $stdout.write(report(scores))
47
+ exit 1 unless scores.all? { |s, _| s.passed? }
48
+ end
49
+
50
+ private
51
+
52
+ # @return [Array<OpenLanes::Evals::Case>]
53
+ def selected(options)
54
+ all = Evals.cases(options[:cases] || Evals::DEFAULT_DIR)
55
+ picked = all.select do |kase|
56
+ (options[:agent].nil? || kase.agent == options[:agent]) && (options[:case].nil? || kase.id == options[:case])
57
+ end
58
+ refuse("No eval case matches #{filters(options)}.", 66) if picked.empty?
59
+ picked.map { |kase| kase.at(options[:depth]) }
60
+ rescue Evals::Invalid => e
61
+ refuse(e.message, 65)
62
+ end
63
+
64
+ # @return [String]
65
+ def filters(options) = options.slice(:agent, :case).map { |k, v| "--#{k} #{v}" }.join(" ").then { it.empty? ? "(none)" : it }
66
+
67
+ # @return [Array<Array(OpenLanes::Evals::Score, OpenLanes::Evals::Run)>]
68
+ def offline(cases, kind)
69
+ cases.filter_map do |kase|
70
+ recording = kase.recording(kind)
71
+ unless recording
72
+ UI.line("#{kase} has no #{kind}/ recording, skipped")
73
+ next
74
+ end
75
+ [Evals.score_recording(kase, kind.to_sym), recording.run]
76
+ end
77
+ end
78
+
79
+ # @return [Array<Array(OpenLanes::Evals::Score, OpenLanes::Evals::Run)>]
80
+ def live(cases, options)
81
+ credentials_warning
82
+ cap = options[:max_tokens] || Evals::Live::DEFAULT_MAX_TOKENS
83
+ cases.map do |kase|
84
+ UI.line("running #{kase} at #{kase.depth}, capped at #{cap} tokens")
85
+ outcome = Evals.run_live(kase, max_tokens: cap)
86
+ result = outcome.result
87
+ [outcome.score, Evals::Run.new(seconds: result.seconds, tokens: result.fresh)]
88
+ end
89
+ end
90
+
91
+ # The table, then what failed and why: the deliverable, on STDOUT.
92
+ #
93
+ # @return [String]
94
+ def report(scores)
95
+ rows = scores.map do |score, run|
96
+ kase = score.kase
97
+ [kase.id, kase.agent, kase.depth, score.tally, run.seconds&.round || "-", run.tokens || "-",
98
+ score.passed? ? "PASS" : "FAIL"]
99
+ end
100
+ table = UI.table(rows, header: %w[Case Agent Depth Checks Seconds Tokens Result])
101
+ details = scores.reject { |s, _| s.passed? }.map do |score, _|
102
+ lines = score.failures.map { |v| " - #{v.name}: #{v.detail}" }
103
+ lines = [" - no checks apply at #{score.kase.depth}"] if lines.empty?
104
+ "#{score.kase} FAIL\n#{lines.join("\n")}\n"
105
+ end
106
+ [table, *details].join("\n")
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module CLI
5
+ # `openlanes index` — the plans as one browsable page.
6
+ class Index < Base
7
+ desc "Create or update .plans/INDEX.md — a global index of every plan."
8
+
9
+ option :output,
10
+ aliases: ["-o"],
11
+ desc: "Write somewhere other than <plans>/INDEX.md; - for STDOUT"
12
+ option :project,
13
+ aliases: ["-p"],
14
+ desc: "Heading for the page, default: the repository's directory name"
15
+
16
+ example [
17
+ " # write .plans/INDEX.md",
18
+ "-o - # print it instead",
19
+ "-p 'Equilibris App' # override the heading"
20
+ ]
21
+
22
+ # @param options [Hash]
23
+ # @return [void]
24
+ def call(**options)
25
+ index = OpenLanes::Plans.index(tree: tree_for(options), project: options[:project])
26
+
27
+ if options[:output] == "-"
28
+ $stdout.write(index.render)
29
+ return
30
+ end
31
+
32
+ path = index.write(typed_path(options[:output]))
33
+ puts path
34
+ return if quiet?(options)
35
+
36
+ success("Wrote #{path}\n\nRegenerate it after any `resync dirs`, which renames folders\nand would otherwise leave every link here pointing at nothing.")
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "tty-prompt"
5
+
6
+ module OpenLanes
7
+ module CLI
8
+ # `openlanes init` — make a directory a project: `.openlanes.yml` and `.plans/`.
9
+ #
10
+ # It decides its own root rather than searching for one, because it is
11
+ # the command that creates what the search looks for.
12
+ class Init < Base
13
+ standalone!
14
+ desc "Write .openlanes.yml at the project root and create the plans directory"
15
+
16
+ option :project, desc: "Heading for INDEX.md and similar (default: the root's directory name)"
17
+ option :lane,
18
+ values: Plans::Spec::LANES.map(&:to_s),
19
+ desc: "Default lane for `openlanes create`: full, plan or quick (default: full)"
20
+ option :frontend,
21
+ type: :boolean,
22
+ desc: "Whether plans have a front end; leave it out to let each plan decide"
23
+ option :jobs, aliases: ["-j"], desc: "How many plans `openlanes run` works on at once (default: cores - 2)"
24
+ option :yes,
25
+ type: :boolean,
26
+ default: false,
27
+ aliases: ["-y"],
28
+ desc: "Ask nothing; take the flags and the defaults"
29
+ dry_run_option "Write nothing; print the file that would be written"
30
+ option :overwrite,
31
+ type: :boolean,
32
+ default: false,
33
+ desc: "Replace an existing .openlanes.yml, which is otherwise kept"
34
+
35
+ example [
36
+ " # ask for each value on a terminal",
37
+ "-y --project 'Equilibris App' # ask nothing",
38
+ "-y --lane plan --no-frontend -j 4 # a command-line tool, four plans at once",
39
+ "-n # show the file, write nothing"
40
+ ]
41
+
42
+ # @param options [Hash]
43
+ # @return [void]
44
+ def call(**options)
45
+ quiet?(options)
46
+ root = Project.init_root
47
+ path = File.join(root, Config::FILE)
48
+ plans = File.expand_path(options.fetch(:dir, OpenLanes::PLANS_DIR), root)
49
+ keep = File.file?(path) && !options[:overwrite]
50
+
51
+ config = keep ? Config.load(root) : settings(root, options)
52
+ return preview(path, plans, config, keep) if dry_run?(options)
53
+ return unless keep || confirmed?(path, options)
54
+
55
+ File.write(path, config.to_yaml) unless keep
56
+ FileUtils.mkdir_p(plans)
57
+ puts path
58
+ report(path, plans, keep, options)
59
+ rescue OpenLanes::Error => e
60
+ refuse(e.message, 65)
61
+ end
62
+
63
+ private
64
+
65
+ # The four values, each from its flag, else asked on a terminal, else
66
+ # its default. The result is parsed back as the file would be, so
67
+ # `init` cannot write a file every other command then refuses.
68
+ #
69
+ # @param root [String]
70
+ # @param options [Hash]
71
+ # @return [OpenLanes::Config]
72
+ def settings(root, options)
73
+ ask = interactive?(options)
74
+ project = options[:project] || (ask ? prompt.ask("Project name:", default: File.basename(root)) : File.basename(root))
75
+ lane = options[:lane] || (ask ? prompt.select("Default lane for new plans:", Plans::Spec::LANES.map(&:to_s)) : "full")
76
+ frontend = options.key?(:frontend) ? options[:frontend] : (ask_frontend if ask)
77
+ jobs = options[:jobs] || (prompt.ask("Plans to run at once (blank: cores - 2):") if ask)
78
+
79
+ draft = Config.new(project:, lane: lane.to_sym, frontend:, jobs: count(jobs))
80
+ Config.parse(draft.to_yaml, path: File.join(root, Config::FILE))
81
+ end
82
+
83
+ # @return [Boolean, nil]
84
+ def ask_frontend
85
+ choice = prompt.select("Do plans have a front end?", ["each plan decides", "YES", "NO"])
86
+ { "YES" => true, "NO" => false }[choice]
87
+ end
88
+
89
+ # @param value [String, Integer, nil]
90
+ # @return [Integer, nil]
91
+ def count(value)
92
+ return nil if value.nil? || value.to_s.strip.empty?
93
+ return value.to_s.to_i if value.to_s.match?(/\A[1-9]\d*\z/)
94
+
95
+ refuse("--jobs must be a whole number above zero, not #{value.inspect}.", 64)
96
+ end
97
+
98
+ # @param path [String]
99
+ # @param options [Hash]
100
+ # @return [Boolean]
101
+ def confirmed?(path, options)
102
+ return true unless interactive?(options)
103
+ return true if prompt.select("Write #{path}?", %w[YES NO]) == "YES"
104
+
105
+ warn("Nothing was written.") unless quiet?(options)
106
+ false
107
+ end
108
+
109
+ # Prompts only for a person at a terminal who did not say -y: a script
110
+ # or a pipe gets the flags and the defaults, never a question it
111
+ # cannot answer.
112
+ #
113
+ # @param options [Hash]
114
+ # @return [Boolean]
115
+ def interactive?(options) = !options[:yes] && tty?
116
+
117
+ # @return [Boolean]
118
+ def tty? = $stdin.tty? && $stderr.tty?
119
+
120
+ # A named seam, so a spec can answer the questions.
121
+ #
122
+ # @return [TTY::Prompt]
123
+ def prompt
124
+ @prompt ||= TTY::Prompt.new(output: $stderr)
125
+ end
126
+
127
+ # @return [void]
128
+ def preview(path, plans, config, keep)
129
+ $stdout.write(config.to_yaml)
130
+ warn("Dry run — nothing was written.\n" \
131
+ "#{keep ? "Would keep" : "Would write"} #{path}" \
132
+ "#{"\nWould create #{plans}" unless File.directory?(plans)}\n\n" \
133
+ "Re-run without --dry-run to apply.")
134
+ end
135
+
136
+ # @return [void]
137
+ def report(path, plans, keep, options)
138
+ return if quiet?(options)
139
+
140
+ success("#{keep ? "Kept" : "Wrote"} #{path}#{" (--overwrite replaces it)" if keep}\n" \
141
+ "Plans live in #{plans}\n\nNext: openlanes create <a few words>")
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module CLI
5
+ # `openlanes linear …`
6
+ module Linear
7
+ # Shared by both linear commands: the team is which workspace this is
8
+ # about, so it is an argument rather than a flag. There is no default —
9
+ # a tool that picks a team for you when you forget to say which is a
10
+ # tool that files a quarter of somebody's work in the wrong place.
11
+ class Team < Base
12
+ def self.inherited(klass)
13
+ super
14
+ klass.argument :team,
15
+ required: true,
16
+ desc: "The Linear team key that prefixes its issues, e.g. TAX"
17
+ end
18
+
19
+ private
20
+
21
+ # The key is checked before anything reaches for a token, so a typo in
22
+ # the team name reports the typo rather than an authentication
23
+ # problem the user does not have.
24
+ #
25
+ # @param team [String]
26
+ # @return [OpenLanes::Linear::Survey]
27
+ def survey_for(team, tree)
28
+ key = OpenLanes::Linear.key!(team)
29
+ api = OpenLanes::Linear::API.new(token: OpenLanes::Linear::API.token_from_env)
30
+ projects = UI.spinning("Listing #{key} projects") { api.projects(api.team(key)[:id]) }
31
+ [api, OpenLanes::Linear::Survey.new(tree:, projects:)]
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module CLI
5
+ # `openlanes linear …`
6
+ module Linear
7
+ # `linear import` — the plans, as Linear issues under one of your projects.
8
+ class Import < Team
9
+ desc "Create Linear issues from the plans: one per folder, one child per work unit"
10
+
11
+ option :project,
12
+ aliases: ["-p", "--project-url", "--project-id"],
13
+ desc: "The project to file everything under: its URL, its name, or its id"
14
+ dry_run_option "Send nothing to Linear; print what would be created and updated"
15
+ option :format,
16
+ default: "text",
17
+ values: %w[text json],
18
+ desc: "json emits the exact arguments the Linear MCP tools take"
19
+ option :since,
20
+ aliases: ["-s"],
21
+ desc: "Skip plans numbered below this, e.g. 010.00"
22
+ option :status,
23
+ desc: "Only plans in these states: a comma-separated list of status keys"
24
+ option :update_unchanged,
25
+ type: :boolean,
26
+ default: false,
27
+ desc: "Update everything, whether the plan has changed or not"
28
+
29
+ example [
30
+ "TAX -p 'US Tax Law: Self Contained Ruby Gem' # list what would be created",
31
+ "TAX -p https://linear.app/acme/project/… # a URL works too",
32
+ "TAX -p 'Ruby Gem' -n # preview, sending nothing",
33
+ "TAX -p 'Ruby Gem' --format json # hand it to the MCP transport instead",
34
+ "TAX -p 'Ruby Gem' --since 010.00 # only the recent plans",
35
+ "TAX -p 'Ruby Gem' --status building,in_review"
36
+ ]
37
+
38
+ # @param team [String]
39
+ # @param options [Hash]
40
+ # @return [void]
41
+ def call(team:, **options)
42
+ tree = tree_for(options)
43
+ require_project!(options)
44
+ import = build(tree, team, resolve_project(team, tree, options), options)
45
+
46
+ return $stdout.puts(import.to_json) if options[:format] == "json"
47
+
48
+ if import.pending.empty?
49
+ success("Linear is already in step with #{tree.dir}.") unless quiet?(options)
50
+ return
51
+ end
52
+
53
+ preview(import, options)
54
+ rescue OpenLanes::Error => e
55
+ refuse(e.message, 69)
56
+ end
57
+
58
+ private
59
+
60
+ # Looking the project up costs a token, and the whole point of
61
+ # `--format json` is to work without one. A name needs no lookup — the
62
+ # MCP server resolves a project by name itself — so only a URL or an
63
+ # id, which do not carry a name, force the network.
64
+ #
65
+ # @return [Hash] `{"id", "name", "url"}`
66
+ def resolve_project(team, tree, options)
67
+ reference = options[:project].to_s
68
+ looks_up = reference.match?(%r{\Ahttps?://}) || reference.match?(/\A[0-9a-f-]{32,}\z/)
69
+ return { "id" => nil, "name" => reference, "url" => nil } if !looks_up && offline?
70
+
71
+ _api, survey = survey_for(team, tree)
72
+ survey.project(reference)
73
+ end
74
+
75
+ # @return [Boolean]
76
+ def offline? = OpenLanes::Linear::API.token_from_env.nil?
77
+
78
+ # @param options [Hash]
79
+ # @return [void]
80
+ def require_project!(options)
81
+ return if options[:project]
82
+
83
+ raise OpenLanes::Error,
84
+ "which project? Pass -p with a project's URL, name or id.\n\n" \
85
+ "This never creates one: a team's project list is something you curated, and " \
86
+ "every plan is filed under one you named.\n\n " \
87
+ "openlanes linear projects <TEAM> lists them"
88
+ end
89
+
90
+ # @return [OpenLanes::Linear::Import]
91
+ def build(tree, team, project, options)
92
+ OpenLanes::Linear::Import.new(tree:,
93
+ team: OpenLanes::Linear.key!(team),
94
+ project:,
95
+ since: options[:since],
96
+ statuses: statuses(options),
97
+ update_unchanged: options.fetch(:update_unchanged, false))
98
+ end
99
+
100
+ # @param options [Hash]
101
+ # @return [Array<Symbol>, nil]
102
+ def statuses(options)
103
+ return nil unless options[:status]
104
+
105
+ options[:status].to_s.split(",").map { |word|
106
+ OpenLanes::Plans.status(word.strip)&.key ||
107
+ raise(OpenLanes::Error, "no such state: #{word.strip}")
108
+ }
109
+ end
110
+
111
+ # @param import [OpenLanes::Linear::Import]
112
+ # @param options [Hash]
113
+ # @return [void]
114
+ def preview(import, options)
115
+ import.pending.each { |action| puts row(action) }
116
+ return if quiet?(options)
117
+
118
+ import.pending.each { |action| say(line(action)) }
119
+ report_unplaced(import)
120
+ what = "Linear change#{"s" unless import.pending.size == 1}"
121
+ return dry_run_footer(import.pending.size, what) if dry_run?(options)
122
+
123
+ # There is no push of its own here yet: the MCP transport is what
124
+ # applies these. Saying so beats implying something was sent.
125
+ warn("#{import.pending.size} #{what} pending — nothing was sent.\n" \
126
+ "This command does not write to Linear itself: pipe --format json to the Linear MCP tools.")
127
+ end
128
+
129
+ # @param action [OpenLanes::Linear::Action]
130
+ # @return [String]
131
+ def row(action)
132
+ [action.op, action.kind, action.ordinal, action.unit || "-",
133
+ action.identifier || "-", action.title].join("\t")
134
+ end
135
+
136
+ # @param action [OpenLanes::Linear::Action]
137
+ # @return [String]
138
+ def line(action)
139
+ verb = paint(action.op.to_s.ljust(6), action.op == :create ? :green : :yellow)
140
+ indent = action.child? ? " " : ""
141
+ "#{verb} #{indent}#{action.title} #{paint("(#{action.reason})", :bright_black)}"
142
+ end
143
+
144
+ # @param import [OpenLanes::Linear::Import]
145
+ # @return [void]
146
+ def report_unplaced(import)
147
+ return if import.unplaced.empty?
148
+
149
+ listed = import.unplaced.map { |status, ordinals|
150
+ " #{status} — #{ordinals.join(", ")}\n#{wrapped(OpenLanes::Linear.reason_unplaced(status))}"
151
+ }
152
+ warn("Not imported, because nothing here knows where they belong:\n\n" \
153
+ "#{listed.join("\n\n")}\n\n" \
154
+ "Decide where they go on your board and add it to Linear::PLACEMENTS.")
155
+ end
156
+
157
+ # A box re-wraps a line that overruns it, and the wrapped remainder
158
+ # comes back at column zero — which reads as a new entry rather than
159
+ # the continuation of one. Wrapping it here keeps the indent.
160
+ #
161
+ # @param text [String]
162
+ # @param width [Integer] narrower than the narrowest box
163
+ # @return [String]
164
+ def wrapped(text, width: 58)
165
+ text.split.each_with_object([+""]) { |word, lines|
166
+ lines << +"" if lines.last.length + word.length + 1 > width
167
+ lines.last << " " unless lines.last.empty?
168
+ lines.last << word
169
+ }.map { |line| " #{line}" }.join("\n")
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module CLI
5
+ # `openlanes linear …`
6
+ module Linear
7
+ # `linear projects` — what the team already has.
8
+ class Projects < Team
9
+ desc "List the Linear projects a team owns, and which plans they already cover"
10
+
11
+ example [
12
+ "TAX # every project the team owns, matched against .plans"
13
+ ]
14
+
15
+ # @param team [String]
16
+ # @param options [Hash]
17
+ # @return [void]
18
+ def call(team:, **options)
19
+ tree = tree_for(options)
20
+ _api, survey = survey_for(team, tree)
21
+
22
+ survey.projects.each { |project| puts row(project) }
23
+ return if quiet?(options)
24
+
25
+ survey.projects.each do |project|
26
+ say("#{paint(project["name"], :cyan)}\n #{paint(project["url"].to_s, :bright_black)}")
27
+ end
28
+ report_descriptions(survey)
29
+ rescue OpenLanes::Error => e
30
+ refuse(e.message, 69)
31
+ end
32
+
33
+ private
34
+
35
+ # @param project [Hash]
36
+ # @return [String]
37
+ def row(project)
38
+ [project["name"], project.dig("status", "name") || "-", project["url"]].join("\t")
39
+ end
40
+
41
+ # @param survey [OpenLanes::Linear::Survey]
42
+ # @return [void]
43
+ def report_descriptions(survey)
44
+ return if survey.undescribed.empty?
45
+
46
+ warn("#{survey.undescribed.size} of these say nothing about themselves:\n\n" \
47
+ "#{survey.undescribed.map { |p| " #{p["name"]}" }.join("\n")}\n\n" \
48
+ "A name is three or four words and half of them are the company's. If you " \
49
+ "want anything here to reason about which project a plan belongs to, that " \
50
+ "reasoning has to have a sentence to read.")
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module CLI
5
+ # `openlanes list-plans` — backs /plan-status.
6
+ class ListPlans < Base
7
+ desc "Print every plan, its state, and its pull requests"
8
+
9
+ example ["", "-D .plans", "| less -R"]
10
+
11
+ # @param options [Hash]
12
+ # @return [void]
13
+ def call(**options)
14
+ reporter = Presentation.reporter(tree: tree_for(options))
15
+ $stdout.write(reporter.render)
16
+
17
+ exit 1 unless reporter.inconsistent.empty?
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module CLI
5
+ # `openlanes mail` — the two halves of a pair talking through the plan
6
+ # folder. See {OpenLanes::Plans::Mailbox} for why a file rather than a session.
7
+ module Mail
8
+ # Both subcommands name a plan and need its folder.
9
+ module Locating
10
+ private
11
+
12
+ # An unknown number is refused rather than given an empty mailbox:
13
+ # an agent polling the wrong plan for a whole round would read
14
+ # silence as "my partner has nothing to say".
15
+ #
16
+ # @param options [Hash]
17
+ # @return [OpenLanes::Plans::Mailbox]
18
+ def mailbox_for(options)
19
+ tree = tree_for(options)
20
+ ordinal = Plans::Ordinal.parse(options[:plan].to_s)
21
+ subject = ordinal && tree.find(ordinal)
22
+ unless subject
23
+ refuse("No plan #{options[:plan]} in #{tree.dir}.\n\nKnown: #{tree.ordinals.join(", ")}", 66)
24
+ end
25
+
26
+ Plans.mailbox(subject.feature.path)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module CLI
5
+ module Mail
6
+ # `openlanes mail read` — what is waiting for one agent.
7
+ #
8
+ # Nothing waiting is the ordinary result of a poll, so it is not an
9
+ # error and not even STDOUT: the messages are the deliverable, and an
10
+ # empty one is empty.
11
+ class Read < Base
12
+ include Locating
13
+
14
+ desc "Print the messages waiting for an agent in a plan's mailbox"
15
+
16
+ option :plan, required: true, desc: "Which plan: NNN or NNN.MM"
17
+ option :for, required: true, desc: "The agent reading"
18
+ option :after, default: "0", desc: "Only messages numbered above this, so a poll shows what is new"
19
+
20
+ example [
21
+ "--plan 003 --for rey-frontend # everything addressed to rey",
22
+ "--plan 003 --for rey-frontend --after 4 # only what arrived since #4"
23
+ ]
24
+
25
+ # @param options [Hash]
26
+ # @return [void]
27
+ def call(**options)
28
+ quiet?(options)
29
+ mailbox = mailbox_for(options)
30
+ after = options[:after].to_i
31
+ waiting = mailbox.for(options[:for], after:)
32
+
33
+ if waiting.empty?
34
+ UI.line("Nothing for #{options[:for]}#{" after ##{after}" if after.positive?}") unless quiet?(options)
35
+ return
36
+ end
37
+
38
+ puts waiting.join("\n")
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end