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,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module CLI
5
+ # `openlanes agents …` — the specialist roster.
6
+ module Agents
7
+ # `openlanes agents list`
8
+ class List < Dry::CLI::Command
9
+ include UI
10
+
11
+ desc "List every specialist, what it handles, and what it advances to"
12
+
13
+ example ["", "| less -R"]
14
+
15
+ # @return [void]
16
+ def call(**) = $stdout.write(OpenLanes::Agents.roster.list)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,263 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/cli"
4
+
5
+ module OpenLanes
6
+ module CLI
7
+ # Shared flags and the plumbing every command needs.
8
+ class Base < Dry::CLI::Command
9
+ include UI
10
+
11
+ # dry-cli gathers every token a command did not declare an argument for
12
+ # into :args, and hands it over without a word. `openlanes docs
13
+ # .plans/README.md` — the `-o` forgotten — therefore ran with every
14
+ # default, wrote the conventions to the default path, and printed
15
+ # success for a file the caller never named.
16
+ #
17
+ # Prepended rather than called from each `call`, because the command
18
+ # that forgets this check is the one that needed it.
19
+ module RefuseStrayArguments
20
+ # @param options [Hash]
21
+ # @return [void]
22
+ def call(**options)
23
+ stray = Array(options[:args]) - self.class.arguments.flat_map { Array(options[it.name.to_sym]) }
24
+ unless stray.empty?
25
+ refuse("This command takes no argument like #{stray.join(" ")}.\n" \
26
+ "An option it does take may be the one you meant: run it with -h.",
27
+ 64)
28
+ end
29
+
30
+ super
31
+ end
32
+ end
33
+
34
+ # Every project command starts at the project root, wherever it was
35
+ # typed: a `.plans` found relative to a subdirectory is a `.plans` that
36
+ # does not exist, and the refusal that follows names the wrong problem.
37
+ #
38
+ # Prepended for the same reason as {RefuseStrayArguments}: the command
39
+ # that forgets to look is the one that runs against the wrong tree.
40
+ module EnterProject
41
+ # @param options [Hash]
42
+ # @return [void]
43
+ def call(**options)
44
+ enter_project(options) unless self.class.standalone?
45
+ super
46
+ end
47
+ end
48
+
49
+ # Marks a command that needs no project — reference output, the agent
50
+ # roster, completion — so it runs from anywhere without the search.
51
+ #
52
+ # @return [void]
53
+ def self.standalone! = @standalone = true
54
+
55
+ # @return [Boolean]
56
+ def self.standalone? = @standalone == true
57
+
58
+ # The name this command is typed as, and the key its section takes
59
+ # under `commands:` in `.openlanes.yml`: "run", "resync dirs", "list-plans".
60
+ #
61
+ # @return [String, nil] nil for a class with no name, as in a spec
62
+ def self.command_name
63
+ return nil unless name
64
+
65
+ name.delete_prefix("OpenLanes::CLI::").split("::")
66
+ .map { OpenLanes.inflector.underscore(it).tr("_", "-") }.join(" ")
67
+ end
68
+
69
+ # dry-cli merges what was typed over these, so a `.openlanes.yml` value
70
+ # placed here sits exactly between the flag and the built-in default.
71
+ # A file that cannot be read adds nothing at this point; {#enter_project}
72
+ # reads it again and refuses with the reason.
73
+ #
74
+ # @return [Hash{Symbol => Object}]
75
+ def self.default_params
76
+ return super if standalone? || command_name.nil?
77
+
78
+ root = Project.root or return super
79
+ known = options.map(&:name)
80
+ super.merge(OpenLanes::Config.load(root).for(command_name).slice(*known))
81
+ rescue OpenLanes::Error
82
+ super
83
+ end
84
+
85
+ # Every `commands:` section must name a command that exists, and every
86
+ # key in it an option that command takes, with a value it accepts. Each
87
+ # project command checks the whole file, not only its own section: a
88
+ # typo under a command you rarely run is still a typo.
89
+ #
90
+ # @param config [OpenLanes::Config]
91
+ # @return [void]
92
+ # @raise [OpenLanes::Error]
93
+ def self.check_config!(config)
94
+ config.commands.each do |name, settings|
95
+ command = OpenLanes::CLI.get(name.split).then { it.found? ? it.command : nil }
96
+ unless command.respond_to?(:command_name) && command.command_name == name
97
+ raise OpenLanes::Error, "#{config.path}: commands.#{name}: no such command"
98
+ end
99
+
100
+ settings.each { |key, value| check_option!(config, command, name, key, value) }
101
+ end
102
+ end
103
+
104
+ # @return [void]
105
+ def self.check_option!(config, command, name, key, value)
106
+ where = "#{config.path}: commands.#{name}.#{key}"
107
+ option = command.options.find { it.name == key }
108
+ raise OpenLanes::Error, "#{where}: #{name} takes no such option" unless option
109
+ raise OpenLanes::Error, "#{where}: a preview is asked for each time, never set as a default" if key == :dry_run
110
+
111
+ if option.boolean? && ![true, false].include?(value)
112
+ raise OpenLanes::Error, "#{where}: #{value.inspect} is not true or false"
113
+ end
114
+ return unless option.values && !option.values.map(&:to_s).include?(value.to_s)
115
+
116
+ raise OpenLanes::Error, "#{where}: #{value.inspect} is not one of #{option.values.join(", ")}"
117
+ end
118
+ private_class_method :check_option!
119
+
120
+ def self.inherited(klass)
121
+ super
122
+ klass.prepend(EnterProject)
123
+ klass.prepend(RefuseStrayArguments)
124
+ klass.option :dir,
125
+ default: OpenLanes::PLANS_DIR,
126
+ aliases: ["-D"],
127
+ desc: "The .plans directory"
128
+ klass.option :quiet,
129
+ type: :boolean,
130
+ default: false,
131
+ aliases: ["-q"],
132
+ desc: "Suppress progress output on STDERR"
133
+ end
134
+
135
+ # Declares `-n` / `--dry-run` on a command that changes something.
136
+ #
137
+ # @param desc [String] what the preview prints instead of doing
138
+ # @return [void]
139
+ def self.dry_run_option(desc)
140
+ option :dry_run,
141
+ type: :boolean,
142
+ default: false,
143
+ aliases: ["-n"],
144
+ desc:
145
+ end
146
+
147
+ private
148
+
149
+ # @param options [Hash]
150
+ # @return [OpenLanes::Plans::Tree]
151
+ def tree_for(options)
152
+ tree = Plans.tree(options.fetch(:dir, OpenLanes::PLANS_DIR))
153
+ unless tree.exist?
154
+ refuse("No #{OpenLanes::PLANS_DIR} directory at\n#{tree.dir}\n\n" \
155
+ "Run `openlanes init` to create it, or pass -D.",
156
+ 66)
157
+ end
158
+ tree
159
+ end
160
+
161
+ # The directory the command was typed in, before {EnterProject} moved
162
+ # to the root. A path the user typed (`--prompt notes.md`) means a file
163
+ # beside them, not one beside `.plans`.
164
+ #
165
+ # @return [String]
166
+ def origin = @origin || Dir.pwd
167
+
168
+ # @param path [String, nil]
169
+ # @return [String, nil] +path+ as the user meant it, from {#origin}
170
+ def typed_path(path) = path && File.expand_path(path, origin)
171
+
172
+ # Finds the root and moves there, saying so when that is somewhere
173
+ # else. `-D` and `PLANS_DIR` are relative, so they resolve against the
174
+ # root from here on.
175
+ #
176
+ # @param options [Hash]
177
+ # @return [void]
178
+ def enter_project(options)
179
+ @origin = Dir.pwd
180
+ root = Project.root(from: @origin)
181
+ unless root
182
+ refuse("No project here: nothing in #{@origin} or above it holds " \
183
+ "#{Project::FILE}, #{OpenLanes::PLANS_DIR}/ or .git.\n\n" \
184
+ "Run `openlanes init` to make this directory one.",
185
+ 66)
186
+ end
187
+ check_config(root)
188
+ return if root == Project.expand(@origin)
189
+
190
+ Dir.chdir(root)
191
+ UI.line("Project root: #{root}") unless options.fetch(:quiet, false)
192
+ end
193
+
194
+ # @param root [String]
195
+ # @return [void]
196
+ def check_config(root)
197
+ self.class.check_config!(OpenLanes::Config.load(root))
198
+ rescue OpenLanes::Error => e
199
+ refuse(e.message, 65)
200
+ end
201
+
202
+ # Report and stop, in the shape every refusal here takes. The statuses
203
+ # follow sysexits where one fits: 64 usage, 65 bad data, 66 no input,
204
+ # 69 a service (GitHub, Linear) refused us.
205
+ #
206
+ # @param message [String]
207
+ # @param status [Integer]
208
+ # @return [void] never returns
209
+ def refuse(message, status)
210
+ error(message)
211
+ exit status
212
+ end
213
+
214
+ # @param options [Hash]
215
+ # @return [Boolean]
216
+ def quiet?(options)
217
+ quiet = options.fetch(:quiet, false)
218
+ UI.quiet = quiet
219
+ quiet
220
+ end
221
+
222
+ # `-n` / `--dry-run`. A command does the work it is named for, so the
223
+ # preview is what has to be asked for, never the work itself.
224
+ #
225
+ # @param options [Hash]
226
+ # @return [Boolean]
227
+ def dry_run?(options) = options.fetch(:dry_run, false)
228
+
229
+ # `claude` prefers a credential in the environment to a claude.ai login,
230
+ # so a project `.env` that direnv loads on `cd` can redirect every agent
231
+ # a run spawns to a key meant for the application itself.
232
+ #
233
+ # Say it once, before the run starts, because the failure arrives three
234
+ # minutes later, once per agent, and reads like an agent problem rather
235
+ # than an environment one.
236
+ #
237
+ # @return [void]
238
+ def credentials_warning
239
+ names = Execution::Executor.foreign_credentials
240
+ return if names.empty?
241
+
242
+ one = names.size == 1
243
+ warn("#{names.join(" and ")} #{one ? "is" : "are"} set in this shell, so every agent authenticates " \
244
+ "with #{one ? "it" : "them"} rather than with your claude.ai login.\n" \
245
+ "A stale one fails every agent in the run with `401 API key is invalid`, " \
246
+ "three minutes at a time.\n" \
247
+ "If it came from a project .env, run `unset #{names.join(" ")}` first.")
248
+ end
249
+
250
+ # The line every dry run ends with, so nobody mistakes a preview for the
251
+ # thing having happened.
252
+ #
253
+ # @param count [Integer] how much work is pending
254
+ # @param what [String]
255
+ # @return [void]
256
+ def dry_run_footer(count, what)
257
+ return if count.zero?
258
+
259
+ warn("#{count} #{what} pending — nothing was changed.\nRe-run without --dry-run to apply.")
260
+ end
261
+ end
262
+ end
263
+ end
@@ -0,0 +1,354 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module OpenLanes
6
+ module CLI
7
+ # `openlanes create <words…>` — backs /spec-create.
8
+ class Create < Base
9
+ desc "Create the next numbered plan folder"
10
+
11
+ argument :words,
12
+ type: :array,
13
+ required: false,
14
+ default: [],
15
+ desc:
16
+ "The topic, two to five words; becomes the folder slug. Omit when --from names it"
17
+
18
+ option :from,
19
+ aliases: ["-f"],
20
+ desc:
21
+ "Seed from a markdown file: its frontmatter `title:` names the folder, and its body fills the spec."
22
+ option :after,
23
+ aliases: ["-a"],
24
+ desc:
25
+ "Create a retroactive plan in the gap after this plan, e.g. 002"
26
+ option :status,
27
+ aliases: ["-s"],
28
+ desc: "Open in a state other than the default"
29
+ option :prs,
30
+ aliases: ["--pr"],
31
+ desc:
32
+ "Document work that already shipped: pull request numbers or URLs, comma separated. Requires --after"
33
+ option :spec,
34
+ type: :boolean,
35
+ default: true,
36
+ desc:
37
+ "With --prs, write spec.md from what the pull requests did. --no-spec records them and stops, which is fast and offline"
38
+ option :draft,
39
+ type: :boolean,
40
+ default: true,
41
+ desc:
42
+ "For a new feature (no --prs), attempt spec.md's four headings from project context via `claude`. --no-draft leaves them bare"
43
+ option :lane,
44
+ aliases: ["-l"],
45
+ values: Plans::Spec::LANES.map(&:to_s),
46
+ desc:
47
+ "Route the plan: full (research, spec, plan, build), plan (skip research and rewriting), " \
48
+ "quick (one agent does a short mechanical task). Written into spec.md's frontmatter"
49
+ option :depth,
50
+ values: Plans::Spec::DEPTHS.map(&:to_s),
51
+ desc: "How hard every phase should think: fast, medium or deep. Written into spec.md's frontmatter"
52
+ option :open,
53
+ type: :boolean,
54
+ default: true,
55
+ desc:
56
+ "Open the new spec.md in the system editor when done (macOS `open`). --no-open leaves it for you to open"
57
+
58
+ # noinspection RubyMismatchedArgumentType
59
+ example [
60
+ "tax rule dsl # 003.00-⚪️ → tax-rule-dsl, spec.md scaffolded and drafted",
61
+ "tax rule dsl --no-draft --no-open # scaffold only, nothing shelled out, nothing opened",
62
+ "--after 002 schedule k1 # 002.01-🕰️-schedule-k1 (documented after the fact)",
63
+ "--after 018 --prs 12,15 verify # …and write spec.md from what those PRs did",
64
+ "--after 018 --pr https://…/pull/12 verify # verify the pull request by number or URL",
65
+ "--status ready billing sync # opens at ⭐️ instead of ⚪️",
66
+ "--from notes/tax-dsl.md # title and opening prose from the file's frontmatter and body",
67
+ "--lane quick split big migration # r2d2-mechanic does it and opens the PR; no research, spec or plan"
68
+ ]
69
+
70
+ # @param words [Array<String>]
71
+ # @param options [Hash]
72
+ # @return [void]
73
+ def call(words:, **options)
74
+ dir = options.fetch(:dir, OpenLanes::PLANS_DIR)
75
+ FileUtils.mkdir_p(dir)
76
+
77
+ words, seed = resolve_seed(words, options)
78
+ prs = fetch_prs(options)
79
+ result =
80
+ Lifecycle.create(dir:,
81
+ words:,
82
+ after: options[:after],
83
+ status: options[:status],
84
+ prs:)
85
+
86
+ result.either(
87
+ ->(path) { created(path, prs, options, seed:) },
88
+ ->(message) { refuse("Could not create the plan:\n#{message}", 65) }
89
+ )
90
+ end
91
+
92
+ private
93
+
94
+ # `--from FILE` trades the words argument for a markdown file: the
95
+ # frontmatter's `title:` names the folder, and the body travels on to
96
+ # {Brief} as the author's own opening statement. Words and a file are
97
+ # two answers to one question — the folder's name — so holding both is
98
+ # refused rather than silently ranked.
99
+ #
100
+ # @param words [Array<String>]
101
+ # @param options [Hash]
102
+ # @return [Array(Array<String>, String, nil)] the slug words, and the
103
+ # seed body when a file supplied them
104
+ def resolve_seed(words, options)
105
+ file = typed_path(options[:from])
106
+ return words, nil unless file
107
+
108
+ unless words.empty?
109
+ refuse(
110
+ "--from names the topic by its frontmatter title;\n" \
111
+ "drop the words, or drop --from",
112
+ 64
113
+ )
114
+ end
115
+ if options[:prs]
116
+ refuse(
117
+ "--prs reconstructs spec.md from the pull requests, so a seed file\n" \
118
+ "would be ignored; use one or the other",
119
+ 64
120
+ )
121
+ end
122
+ unless File.file?(file)
123
+ refuse("Could not read the seed file:\n#{file} does not exist", 66)
124
+ end
125
+
126
+ title, body = parse_seed(File.read(file, encoding: "UTF-8"))
127
+ if title.to_s.strip.empty?
128
+ refuse(
129
+ "The seed file needs a frontmatter title, e.g.\n" \
130
+ "---\ntitle: Tax Rule DSL\n---",
131
+ 65
132
+ )
133
+ end
134
+
135
+ [title.split, body]
136
+ end
137
+
138
+ # Frontmatter that will not parse used to be reported as a missing
139
+ # title, which sends the author looking for a `title:` line that is
140
+ # sitting right there. Say which of the two actually went wrong.
141
+ #
142
+ # @param content [String]
143
+ # @return [Array(String, String)] title and body
144
+ def parse_seed(content)
145
+ meta, body = Frontmatter.split(content)
146
+ [meta["title"].to_s, body.strip]
147
+ rescue Psych::Exception => e
148
+ refuse(
149
+ "The seed file's frontmatter is not valid YAML:\n#{e.message}",
150
+ 65
151
+ )
152
+ end
153
+
154
+ # A retroactive plan documents work that landed *somewhere* in the
155
+ # sequence, and only its author knows where. Guessing would put the
156
+ # number — the one thing that never changes — in the wrong place.
157
+ #
158
+ # @param options [Hash]
159
+ # @return [Array<Hash>, nil]
160
+ def fetch_prs(options)
161
+ return nil unless options[:prs]
162
+
163
+ unless options[:after]
164
+ refuse(
165
+ "--prs documents work that already shipped;\n" \
166
+ "name the plan it landed after with --after, e.g. --after 018",
167
+ 64
168
+ )
169
+ end
170
+
171
+ Vcs.github.pull_requests(Vcs::GitHub.parse_refs(options[:prs]))
172
+ rescue OpenLanes::Error => e
173
+ refuse("Could not read the pull requests:\n#{e.message}", 65)
174
+ end
175
+
176
+ # A plan with recorded pull requests already has its facts — hand it to
177
+ # the writer that reconstructs a specification from a diff. One without
178
+ # them does not exist yet, and reconstructing is not the job; {#brief} is.
179
+ #
180
+ # @param path [String]
181
+ # @param prs [Array<Hash>, nil]
182
+ # @param options [Hash]
183
+ # @return [void]
184
+ def created(path, prs, options, seed: nil)
185
+ from_prs = prs && !prs.empty?
186
+ path =
187
+ if from_prs
188
+ synthesize(path, options) if options.fetch(:spec, true)
189
+ else
190
+ brief(path, options, seed:)
191
+ end || path
192
+ puts path
193
+ return if quiet?(options)
194
+
195
+ feature = Plans::Feature.parse(path)
196
+ success(
197
+ "Created #{File.basename(path)}\n\n" \
198
+ "#{feature.status.emoji} #{feature.status.label} — #{feature.status.note}\n" \
199
+ "#{next_step(path, feature, from_prs:)}"
200
+ )
201
+ end
202
+
203
+ # Hand the folder to the writer that already knows how to write a
204
+ # retroactive specification, then let `resync dirs` decide what the
205
+ # folder has become — 🕰️ is only true while there is no `spec.md`.
206
+ #
207
+ # @param path [String]
208
+ # @param options [Hash]
209
+ # @return [String] the folder's path, which the resync may have renamed
210
+ def synthesize(path, options)
211
+ agent = OpenLanes::Agents.registry.find(OpenLanes::RETROACTIVE_WRITER) or
212
+ return path
213
+ root = options[:root] || File.dirname(path, 2)
214
+
215
+ feature = Plans::Feature.parse(path)
216
+ ok, note = on_agent_row("Writing spec.md from #{UI.paint(File.basename(path).to_s, :yellow)}",
217
+ agent: agent.name,
218
+ plan: feature.ordinal.to_s) do |line|
219
+ Execution.executor(root:).call(agent, Plans.subject(feature), &line)
220
+ end
221
+ warn_about(note) unless ok
222
+
223
+ settle(path)
224
+ end
225
+
226
+ # A folder with no pull requests to reconstruct from is a feature that
227
+ # does not exist yet. {Brief} writes the four headings a human still has
228
+ # to answer, makes a best-effort pass at them from what the project
229
+ # already has on disk, and — unless told not to — opens the result for
230
+ # a human to finish. The folder's state never moves: ⚪️ New only ever
231
+ # claimed that a specification exists, not that it is complete.
232
+ #
233
+ # @param path [String] the path to the folder
234
+ # @param seed [String, nil] the seed body from the `--from` file
235
+ # @param options [Hash] the options hash
236
+ # @return [String] +path+, unchanged
237
+ def brief(path, options, seed: nil)
238
+ feature = Plans::Feature.parse(path)
239
+ return path if feature.status.key == :retroactive
240
+
241
+ root = options[:root] || File.dirname(path, 2)
242
+ brief = Lifecycle.brief(path:, title: feature.title, root:, seed:)
243
+ brief.write_scaffold!
244
+
245
+ if options.fetch(:draft, true)
246
+ # Painted the way Runner::Task#label paints a roster agent, so the
247
+ # half-agent reads as one of them on the terminal.
248
+ label =
249
+ "#{UI.paint(Lifecycle::Brief::AGENT_NAME, :yellow, :bold)} drafting spec.md from #{seed}"
250
+ ok, note = on_agent_row(label, agent: Lifecycle::Brief::AGENT_NAME, plan: feature.ordinal.to_s) { brief.attempt! }
251
+ warn_about_draft(note) unless ok
252
+ end
253
+
254
+ stamp(brief.spec_path, options)
255
+ open_spec(brief.spec_path) if options.fetch(:open, true)
256
+ path
257
+ end
258
+
259
+ # One agent writing spec.md, drawn as a spinner line.
260
+ #
261
+ # @param label [String] the spinner line's text
262
+ # @param agent [String]
263
+ # @param plan [String]
264
+ # @yieldparam line [OpenLanes::UI::Line]
265
+ # @return [Object] the block's value
266
+ def on_agent_row(label, agent:, plan:, &block)
267
+ Execution.on_interrupt do
268
+ UI.concurrently([plan],
269
+ label,
270
+ jobs: 1,
271
+ label: ->(_) { label },
272
+ fields: ->(_) { { plan:, agent: } }) { |_, line| block.call(line) }.first
273
+ end
274
+ end
275
+
276
+ # Writes `--lane` and `--depth` into spec.md's frontmatter, after the
277
+ # draft, so a drafting agent that rewrites the file cannot drop them.
278
+ #
279
+ # @param spec_path [String]
280
+ # @param options [Hash]
281
+ # @return [void]
282
+ def stamp(spec_path, options)
283
+ settings = { "lane" => options[:lane], "depth" => options[:depth] }.compact
284
+ return if settings.empty? || !File.file?(spec_path)
285
+
286
+ meta, body = Frontmatter.split(File.read(spec_path, encoding: "UTF-8"))
287
+ File.write(spec_path, "#{YAML.dump(meta.merge(settings))}---\n#{body}")
288
+ end
289
+
290
+ # @param spec_path [String]
291
+ # @return [void]
292
+ def open_spec(spec_path)
293
+ return unless macos?
294
+
295
+ system("open", spec_path, out: File::NULL, err: File::NULL)
296
+ end
297
+
298
+ # `open` is a macOS command; anywhere else the viewer step is skipped
299
+ # rather than failed. A named seam so a spec can assert the viewer was
300
+ # asked for without inheriting the platform CI happens to run on.
301
+ #
302
+ # @return [Boolean]
303
+ def macos? = RbConfig::CONFIG["host_os"].to_s.match?(/darwin/)
304
+
305
+ # @param path [String]
306
+ # @return [String] where the folder ended up
307
+ def settle(path)
308
+ tree = Plans.tree(File.dirname(path))
309
+ change =
310
+ OpenLanes::Lifecycle::Resync::Dirs
311
+ .new(tree:)
312
+ .call(commit: true)
313
+ .find { |c| c.source == path }
314
+ change ? change.target : path
315
+ end
316
+
317
+ # @param note [String]
318
+ # @return [void]
319
+ def warn_about(note)
320
+ error(
321
+ "The folder was created, but spec.md was not written:\n#{note}\n\n" \
322
+ "The pull requests are recorded. Run `openlanes run` to retry."
323
+ )
324
+ end
325
+
326
+ # @param note [String]
327
+ # @return [void]
328
+ def warn_about_draft(note)
329
+ error(
330
+ "spec.md was scaffolded, but the drafting attempt did not finish:\n#{note}\n\n" \
331
+ "The four headings are there, empty. Fill them in by hand, or hand off to leah-researcher."
332
+ )
333
+ end
334
+
335
+ # @param path [String]
336
+ # @param feature [OpenLanes::Plans::Feature]
337
+ # @return [String]
338
+ def next_step(path, feature, from_prs:)
339
+ spec = File.join(File.basename(path), "spec.md")
340
+ unless feature.status.key == :new &&
341
+ File.file?(File.join(path, "spec.md"))
342
+ return "Next: write #{spec}"
343
+ end
344
+ if from_prs
345
+ return (
346
+ "Next: read #{spec} — it was written from the pull requests, so check it against what actually shipped"
347
+ )
348
+ end
349
+
350
+ "Next: fill in the four headings in #{spec}, then hand off to leah-researcher"
351
+ end
352
+ end
353
+ end
354
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module OpenLanes
6
+ module CLI
7
+ # `openlanes docs` — regenerate the conventions document.
8
+ class Docs < Base
9
+ standalone!
10
+ desc "Generate the conventions document from the state machine itself"
11
+
12
+ # STDOUT unless a file is named. The default used to be a file in the
13
+ # user's home, so an agent running `openlanes docs` to *read* the
14
+ # conventions overwrote ~/.agents/context/workflow.md, untracked and
15
+ # unrecoverable, while the help promised STDOUT.
16
+ option :output,
17
+ aliases: ["-o"],
18
+ desc: "Write here instead of STDOUT; - for STDOUT"
19
+
20
+ example [" # print it", "-o ~/.agents/context/workflow.md # write it"]
21
+
22
+ # @param options [Hash]
23
+ # @return [void]
24
+ def call(**options)
25
+ document = Presentation.documentation
26
+
27
+ path = options[:output]
28
+ if path && path != "-"
29
+ FileUtils.mkdir_p(File.dirname(path))
30
+ File.write(path, document)
31
+ # Argument form, so a path with a space or a quote reaches mdformat whole.
32
+ system("mdformat", "--wrap", "no", path, err: File::NULL, out: File::NULL)
33
+ success("created workflow description in [#{path}]") unless quiet?(options)
34
+ else
35
+ $stdout.write(document)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end