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,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ # The coding agents a plan can be handed to: `claude`, `codex` and `pi`.
5
+ #
6
+ # The harness decides who works which plan and checks what they did; an
7
+ # adapter only knows how to start one particular CLI and how to read what
8
+ # it prints. That is the whole seam, and it is what lets an agent
9
+ # definition say `adapter: codex` without the run loop noticing.
10
+ module Adapters
11
+ # Reasoning effort, in the vocabulary agent frontmatter and spec.md use.
12
+ # Each adapter translates it into its own CLI's words.
13
+ EFFORTS = %w[low medium high xhigh max].freeze
14
+
15
+ # Percent of the context window at which every agent compacts itself
16
+ # (W28), instead of the CLI's default near the limit: a long session's
17
+ # quality drops well before its window is full. Each adapter maps it to
18
+ # its own CLI's setting in {Base#environment}.
19
+ AUTOCOMPACT = 40
20
+
21
+ # Everything an adapter needs to start one agent, and nothing about why.
22
+ #
23
+ # @!attribute [r] prompt
24
+ # @return [String]
25
+ # @!attribute [r] root
26
+ # @return [String] the checkout the agent works in
27
+ # @!attribute [r] model
28
+ # @return [String, nil] nil lets the CLI choose
29
+ # @!attribute [r] effort
30
+ # @return [String, nil] one of {EFFORTS}
31
+ # @!attribute [r] allowed_tools
32
+ # @return [Array<String>]
33
+ # @!attribute [r] denied_tools
34
+ # @return [Array<String>] tool names, plus `Bash(<command>:*)` specifiers
35
+ # @!attribute [r] denied_commands
36
+ # @return [Array<String>] the bare commands behind those specifiers
37
+ # @!attribute [r] network
38
+ # @return [Boolean] whether the agent may reach the internet
39
+ # @!attribute [r] lean
40
+ # @return [Boolean] start without the operator's personal plugins,
41
+ # skills, hooks and MCP servers
42
+ # @!attribute [r] resume
43
+ # @return [String, nil] a session to carry on rather than start afresh
44
+ # @!attribute [r] slash_commands
45
+ # @return [Boolean] keep the CLI's slash commands even when lean, which
46
+ # only the `/compact` sent into a resumed session needs
47
+ Invocation = Data.define(:prompt,
48
+ :root,
49
+ :model,
50
+ :effort,
51
+ :allowed_tools,
52
+ :denied_tools,
53
+ :denied_commands,
54
+ :network,
55
+ :lean,
56
+ :resume,
57
+ :slash_commands) do
58
+ def initialize(allowed_tools: [], denied_tools: [], denied_commands: [], network: false, lean: true,
59
+ model: nil, effort: nil, resume: nil, slash_commands: false, **rest)
60
+ super
61
+ end
62
+ end
63
+
64
+ # The adapter every agent gets unless its definition names another.
65
+ DEFAULT = "claude"
66
+
67
+ module_function
68
+
69
+ # @return [Hash{String => Class}]
70
+ def registry = { "claude" => Claude, "codex" => Codex, "pi" => Pi }
71
+
72
+ # @return [Array<String>]
73
+ def names = registry.keys
74
+
75
+ # @param name [String, Symbol, nil]
76
+ # @return [OpenLanes::Adapters::Base]
77
+ # @raise [OpenLanes::Error] for a name nothing answers to
78
+ def for(name)
79
+ key = (name || DEFAULT).to_s
80
+ klass = registry[key] or raise OpenLanes::Error, "unknown adapter #{key.inspect}; known: #{names.join(", ")}"
81
+ klass.new
82
+ end
83
+
84
+ # @param name [String, Symbol, nil]
85
+ # @return [Boolean]
86
+ def known?(name) = registry.key?((name || DEFAULT).to_s)
87
+
88
+ # @param level [String, Symbol, nil]
89
+ # @return [Boolean]
90
+ def effort?(level) = EFFORTS.include?(level.to_s)
91
+ end
92
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module Agents
5
+ # One specialist, loaded from `~/.agents/agents/<name>.md`.
6
+ #
7
+ # The definition files are the single source of truth for who does what: the
8
+ # frontmatter says which plan states this agent handles and what it may write,
9
+ # and the body is the prompt. Nothing about a specialty is duplicated in Ruby.
10
+ #
11
+ # @!attribute [r] name
12
+ # @return [String]
13
+ # @!attribute [r] description
14
+ # @return [String]
15
+ # @!attribute [r] handles
16
+ # @return [Array<Symbol>] plan states this agent is offered work from
17
+ # @!attribute [r] advances_to
18
+ # @return [Symbol, nil] the state it is expected to reach; nil = read-only
19
+ # @!attribute [r] model
20
+ # @return [String, nil]
21
+ # @!attribute [r] allowed_tools
22
+ # @return [Array<String>]
23
+ # @!attribute [r] may
24
+ # @return [Array<String>] commands lifted from {Executor::FORBIDDEN_COMMANDS}
25
+ # for this agent alone. Nothing in {Executor::UNGRANTABLE} can be lifted.
26
+ # @!attribute [r] timeout
27
+ # @return [Integer, nil] seconds before this agent is abandoned; nil
28
+ # defers to the executor's run-wide default
29
+ # @!attribute [r] ledger
30
+ # @return [Array<String>] the documents this agent signs, in the order
31
+ # it reaches them; the last entry it wrote is where it stands
32
+ # @!attribute [r] rounds
33
+ # @return [Integer] how many times it may be invoked on one plan in one
34
+ # state before the plan parks; a Completed never earns another
35
+ # @!attribute [r] effort
36
+ # @return [String, nil] passed to `claude --effort`
37
+ # @!attribute [r] starts_as
38
+ # @return [Symbol, nil] the state the harness renames the plan into the
39
+ # moment this agent is dispatched, where the topology permits it
40
+ # @!attribute [r] holds_at
41
+ # @return [Symbol, nil] the state the plan takes when this agent completes
42
+ # while its partner on the same plan is still running
43
+ # @!attribute [r] adapter
44
+ # @return [String] which coding agent runs it: `claude`, `codex` or `pi`
45
+ # @!attribute [r] phase
46
+ # @return [String, nil] the word a spec's `phases:` map uses to reach it
47
+ # @!attribute [r] lanes
48
+ # @return [Array<Symbol>] the lanes it works in; empty means every lane
49
+ # @!attribute [r] needs
50
+ # @return [Array<String>] plan files that must hold at least one work-unit
51
+ # heading before it is dispatched
52
+ # @!attribute [r] toggle
53
+ # @return [String, nil] a spec.md frontmatter key that switches it off
54
+ # when false and on when true
55
+ Agent = Data.define(:name,
56
+ :description,
57
+ :handles,
58
+ :advances_to,
59
+ :model,
60
+ :allowed_tools,
61
+ :may,
62
+ :network,
63
+ :timeout,
64
+ :prompt,
65
+ :path,
66
+ :ledger,
67
+ :rounds,
68
+ :effort,
69
+ :starts_as,
70
+ :holds_at,
71
+ :adapter,
72
+ :phase,
73
+ :lanes,
74
+ :needs,
75
+ :toggle) do
76
+ def initialize(ledger: [], rounds: 1, effort: nil, starts_as: nil, holds_at: nil, adapter: Adapters::DEFAULT,
77
+ phase: nil, lanes: [], needs: [], toggle: nil, **rest)
78
+ super
79
+ end
80
+
81
+ # @return [Boolean] whether this agent changes anything on disk
82
+ def read_only? = advances_to.nil?
83
+
84
+ # @param status [OpenLanes::Plans::Status]
85
+ # @return [Boolean]
86
+ def handles?(status) = handles.include?(status.key)
87
+
88
+ # @param lane [Symbol]
89
+ # @return [Boolean]
90
+ def serves?(lane) = lanes.empty? || lanes.include?(lane.to_sym)
91
+
92
+ # The word after the hyphen: `researcher`, `backend`. What the screen's
93
+ # agent column shows, the name being too long for it.
94
+ #
95
+ # @return [String]
96
+ def role = name.split("-", 2).last.to_s
97
+ end
98
+
99
+ # More rounds than this costs tokens and buys nothing: an agent that has
100
+ # not finished in five tries is not going to on the sixth.
101
+ Agent::MAX_ROUNDS = 5
102
+ end
103
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module Agents
5
+ # Which coding agent runs an agent on a particular plan, on which model,
6
+ # thinking how hard: the agent's own frontmatter, overridden by what the
7
+ # plan's author asked for, overridden by what the operator typed.
8
+ #
9
+ # Precedence, highest first:
10
+ #
11
+ # 1. `run --model` (model only)
12
+ # 2. spec.md `phases.<phase>` (adapter, model, effort)
13
+ # 3. spec.md `depth` (effort only)
14
+ # 4. the agent's frontmatter
15
+ #
16
+ # The adapter has the last word on the model, because it is the one that
17
+ # knows the ceiling: a Claude agent asked for Fable runs on Opus.
18
+ #
19
+ # @!attribute [r] adapter
20
+ # @return [OpenLanes::Adapters::Base]
21
+ # @!attribute [r] model
22
+ # @return [String, nil]
23
+ # @!attribute [r] effort
24
+ # @return [String, nil]
25
+ Profile = Data.define(:adapter, :model, :effort) do
26
+ # @return [String] e.g. `claude:opus/high`
27
+ def to_s = "#{adapter.name}:#{model || "default"}#{"/#{effort}" if effort}"
28
+
29
+ # What a narrow column shows: the model, prefixed by the adapter only
30
+ # when it is not the default one.
31
+ #
32
+ # @return [String] e.g. `opus`, `codex:gpt-5-codex`
33
+ def label
34
+ name = model || "default"
35
+ adapter.name == Adapters::DEFAULT ? name : "#{adapter.name}:#{name}"
36
+ end
37
+ end
38
+
39
+ class Profile
40
+ # @param agent [OpenLanes::Agents::Agent]
41
+ # @param spec [OpenLanes::Plans::Spec, nil]
42
+ # @param model [String, nil] what `run --model` typed
43
+ # @return [OpenLanes::Agents::Profile]
44
+ def self.resolve(agent, spec: nil, model: nil)
45
+ override = spec ? spec.override_for(agent.phase) : {}
46
+ adapter = Adapters.for(override["adapter"] || agent.adapter)
47
+ # A model named for one CLI means nothing to another, so switching
48
+ # adapters without naming a model drops the agent's own.
49
+ own_model = override.key?("adapter") && override["adapter"] != agent.adapter ? nil : agent.model
50
+ depth_effort = spec&.depth && Plans::Spec::EFFORT_FOR_DEPTH[spec.depth]
51
+ new(adapter:,
52
+ model: adapter.model(model || override["model"] || own_model),
53
+ effort: override["effort"] || depth_effort || agent.effort)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module Agents
5
+ # Loads and indexes the agent definitions.
6
+ class Registry
7
+ # Where definitions live, unless told otherwise.
8
+ DEFAULT_DIR = File.expand_path("../../../agents", __dir__)
9
+
10
+ # @param dir [String]
11
+ # @param roster [Array<OpenLanes::Agents::Agent>, nil] a pre-selected list, used
12
+ # by {#only} and {#without} to derive a narrower roster; nil (the
13
+ # default) loads every definition in `dir`
14
+ def initialize(dir: DEFAULT_DIR, roster: nil)
15
+ @dir = File.expand_path(dir)
16
+ @all = roster
17
+ end
18
+
19
+ # @return [String]
20
+ attr_reader :dir
21
+
22
+ # @return [Array<OpenLanes::Agents::Agent>] in name order
23
+ def all
24
+ @all ||= Dir.glob(File.join(dir, "*.md")).sort.filter_map { |path| parse(path) }
25
+ end
26
+
27
+ # @param name [String]
28
+ # @return [OpenLanes::Agents::Agent, nil]
29
+ def find(name) = all.find { |a| a.name == name.to_s }
30
+
31
+ # A roster holding only the agents named — what `run --agent` hands the
32
+ # loop, so a restriction typed at the command line restricts assignments
33
+ # and not merely chaining.
34
+ #
35
+ # @param names [Array<String>]
36
+ # @return [OpenLanes::Agents::Registry]
37
+ def only(*names)
38
+ wanted = names.flatten.map(&:to_s)
39
+ self.class.new(dir:, roster: all.select { |a| wanted.include?(a.name) })
40
+ end
41
+
42
+ # A roster without the agents named — what `run --skip` hands the loop.
43
+ # A plan sitting in a skipped agent's state is simply never assigned, the
44
+ # same way a state no agent handles is stepped around.
45
+ #
46
+ # @param names [Array<String>]
47
+ # @return [OpenLanes::Agents::Registry]
48
+ def without(*names)
49
+ unwanted = names.flatten.map(&:to_s)
50
+ self.class.new(dir:, roster: all.reject { |a| unwanted.include?(a.name) })
51
+ end
52
+
53
+ # Every agent the query could mean. An exact name wins outright; failing
54
+ # that the query matches as a prefix, and failing that anywhere in the
55
+ # name, so `leah` finds leah-researcher and `review` finds
56
+ # hansolo-reviewer. A directory or a trailing `.md` is stripped first,
57
+ # because tab completion hands those in.
58
+ #
59
+ # @param query [String]
60
+ # @return [Array<OpenLanes::Agents::Agent>]
61
+ def match(query)
62
+ wanted = File.basename(query.to_s, ".md")
63
+ exact = all.select { |a| a.name == wanted }
64
+ return exact unless exact.empty?
65
+
66
+ prefixed = all.select { |a| a.name.start_with?(wanted) }
67
+ return prefixed unless prefixed.empty?
68
+
69
+ all.select { |a| a.name.include?(wanted) }
70
+ end
71
+
72
+ # Every agent that will act on a plan in this state, in definition order.
73
+ # A read-only agent is never offered work by the loop — it has nothing to
74
+ # advance, so including it would make every round look productive.
75
+ #
76
+ # @param status [OpenLanes::Plans::Status]
77
+ # @return [Array<OpenLanes::Agents::Agent>]
78
+ def for_status(status) = all.select { |a| a.handles?(status) && !a.read_only? }
79
+
80
+ # Everyone on a plan in this state: the agents that work it, plus any
81
+ # agent whose `holds_at` parks the plan here after it finished its half.
82
+ # Rey resuming a plan at 🎨 still needs luke's messages in the mailbox.
83
+ #
84
+ # @param status [OpenLanes::Plans::Status]
85
+ # @return [Array<OpenLanes::Agents::Agent>]
86
+ def team_for(status) = all.select { |a| !a.read_only? && (a.handles?(status) || a.holds_at == status.key) }
87
+
88
+ private
89
+
90
+ # @param path [String]
91
+ # @return [OpenLanes::Agents::Agent, nil]
92
+ def parse(path)
93
+ meta, body = Frontmatter.split(File.read(path, encoding: "UTF-8"))
94
+ return nil if meta["name"].to_s.empty?
95
+
96
+ Agent.new(
97
+ name: meta["name"].to_s,
98
+ description: meta["description"].to_s,
99
+ handles: Array(meta["handles"]).map { |s| s.to_s.to_sym },
100
+ advances_to: meta["advances_to"]&.to_s&.then { |s| s.empty? ? nil : s.to_sym },
101
+ model: meta["model"],
102
+ allowed_tools: Array(meta["allowed_tools"]).map(&:to_s),
103
+ may: Array(meta["may"]).map { |c| c.to_s.strip.squeeze(" ") },
104
+ network: meta["network"] == true,
105
+ timeout: meta["timeout"].to_i.then { |s| s.positive? ? s : nil },
106
+ prompt: body.strip,
107
+ path: path,
108
+ ledger: Array(meta["ledger"]).map(&:to_s),
109
+ rounds: meta["rounds"].to_i.clamp(1, Agent::MAX_ROUNDS),
110
+ effort: effort(meta["effort"], path),
111
+ starts_as: symbol_or_nil(meta["starts_as"]),
112
+ holds_at: symbol_or_nil(meta["holds_at"]),
113
+ adapter: adapter(meta["adapter"], path),
114
+ phase: meta["phase"]&.to_s,
115
+ lanes: Array(meta["lanes"]).map { |l| l.to_s.to_sym },
116
+ needs: Array(meta["needs"]).map(&:to_s),
117
+ toggle: meta["toggle"]&.to_s
118
+ )
119
+ end
120
+
121
+ # A definition is checked in with the code, so a bad value in one is a
122
+ # bug to stop on, not a preference to work around.
123
+ #
124
+ # @param value [Object, nil]
125
+ # @param path [String]
126
+ # @return [String, nil]
127
+ # @raise [OpenLanes::Error]
128
+ def effort(value, path)
129
+ return nil if value.nil?
130
+ return value.to_s if Adapters.effort?(value)
131
+
132
+ raise OpenLanes::Error, "#{File.basename(path)}: effort #{value.inspect} is not one of #{Adapters::EFFORTS.join(", ")}"
133
+ end
134
+
135
+ # @param value [Object, nil]
136
+ # @param path [String]
137
+ # @return [String]
138
+ # @raise [OpenLanes::Error]
139
+ def adapter(value, path)
140
+ return Adapters::DEFAULT if value.nil?
141
+ return value.to_s if Adapters.known?(value)
142
+
143
+ raise OpenLanes::Error, "#{File.basename(path)}: adapter #{value.inspect} is not one of #{Adapters.names.join(", ")}"
144
+ end
145
+
146
+ # @param value [Object, nil]
147
+ # @return [Symbol, nil]
148
+ def symbol_or_nil(value) = value.to_s.empty? ? nil : value.to_s.to_sym
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module Agents
5
+ # `openlanes agents` — who the specialists are and what each is offered
6
+ # work from. Reading one in full is {Viewer}'s job: the definition file is
7
+ # already the whole truth, so `agents describe` opens it rather than
8
+ # restating it.
9
+ #
10
+ # {#list} returns a string and prints nothing, the same bargain {Reporter}
11
+ # makes, so the caller decides where the text goes and the table stays
12
+ # pipeable.
13
+ #
14
+ # Everything here is read from the definition files. Nothing about a
15
+ # specialty is restated in Ruby, because a second copy of "who does what" is
16
+ # a copy that drifts from the frontmatter the loop actually routes on.
17
+ class Roster
18
+ # Columns, so the header and the rows cannot drift apart.
19
+ HEADINGS = ["Agent", "Handles", "Advances to", "Runs on", "Lanes"].freeze
20
+
21
+ # What an agent with no `advances_to` is, in the one word that explains why
22
+ # `run` never offers it work.
23
+ READ_ONLY = "read-only"
24
+
25
+ # What an agent that handles only {StateMachine::SETTLED} states advances
26
+ # to. `run` never starts it; a command does, and `resync` then names the
27
+ # folder by what its files justify.
28
+ BY_CONTENTS = "by contents"
29
+
30
+ # @param agents [OpenLanes::Agents::Registry]
31
+ def initialize(agents: Registry.new)
32
+ @agents = agents
33
+ end
34
+
35
+ # @return [OpenLanes::Agents::Registry]
36
+ attr_reader :agents
37
+
38
+ # One row per agent, in name order, framed by {UI.table}.
39
+ #
40
+ # @return [String] newline-terminated
41
+ def list
42
+ all = agents.all
43
+ return "No agent definitions in #{agents.dir}\n" if all.empty?
44
+
45
+ UI.table(all.map { |agent| row(agent) }, header: HEADINGS)
46
+ end
47
+
48
+ private
49
+
50
+ # A state as its words alone, deliberately without the emoji that every
51
+ # other rendering of a state carries. The table widget pads a cell to a
52
+ # width it measures in characters, while the terminal draws a status emoji
53
+ # in two cells; the variation-selector ones (⭕️ 🅱️ ⚪️ ⭐️ 🕰️) are two
54
+ # characters drawn in two cells, so no single count is right for all of
55
+ # them. A row carrying one lands short or long and the frame stops lining
56
+ # up. The label says the same thing unambiguously, and is worth more than
57
+ # the emoji is. {Reporter} lays its own columns out with {UI.fit} and so
58
+ # keeps the emoji.
59
+ #
60
+ # @param key [Symbol]
61
+ # @return [String]
62
+ def state(key)
63
+ status = Plans::STATUS_BY_KEY[key] or return key.to_s
64
+ status.label
65
+ end
66
+
67
+ # @param agent [OpenLanes::Agents::Agent]
68
+ # @return [String]
69
+ def handles(agent) = agent.handles.map { |key| state(key) }.join(", ")
70
+
71
+ # @param agent [OpenLanes::Agents::Agent]
72
+ # @return [String]
73
+ def advances(agent)
74
+ return state(agent.advances_to) if agent.advances_to
75
+ return BY_CONTENTS if agent.handles.any? && agent.handles.all? { |key| Plans::StateMachine::SETTLED.include?(key) }
76
+
77
+ READ_ONLY
78
+ end
79
+
80
+ # The widget measures and pads the cells, so they arrive painted only.
81
+ #
82
+ # @param agent [OpenLanes::Agents::Agent]
83
+ # @return [Array<String>] one cell per entry in {HEADINGS}
84
+ def row(agent)
85
+ [UI.paint(agent.name, :bright_cyan),
86
+ handles(agent),
87
+ UI.paint(advances(agent), agent.read_only? ? :bright_black : :green),
88
+ UI.paint(runs_on(agent), :bright_black),
89
+ UI.paint(agent.lanes.empty? ? "all" : agent.lanes.join(", "), :bright_black)]
90
+ end
91
+
92
+ # The agent's own settings, before any spec.md overrides them: the
93
+ # model, the effort, and the adapter when it is not the default.
94
+ #
95
+ # @param agent [OpenLanes::Agents::Agent]
96
+ # @return [String] e.g. `opus/high`, `codex:gpt-5-codex/medium`
97
+ def runs_on(agent)
98
+ model = Agents::Profile.resolve(agent).label
99
+ agent.effort ? "#{model}/#{agent.effort}" : model
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module Agents
5
+ # Whether an agent that handles a plan's state should actually be given
6
+ # the plan. Handling the state is necessary, not sufficient: the plan's
7
+ # lane may route around the agent, its spec.md may switch it off, or the
8
+ # files it builds from may hold nothing for it to do.
9
+ #
10
+ # The front end is the case that motivated this. rey-frontend used to be
11
+ # started on every planned folder, and spent ten minutes and five dollars
12
+ # on a plan whose interface was three lines of terminal output.
13
+ module Routing
14
+ # A work unit in a plan file: a second- or third-level heading. A title
15
+ # line does not count, so a `plan-frontend.md` that is only a heading
16
+ # and "no front-end units" starts nobody.
17
+ UNIT_HEADING = /^[ \t]{0,3}\#{2,3}[ \t]+\S/
18
+
19
+ module_function
20
+
21
+ # @param agent [OpenLanes::Agents::Agent]
22
+ # @param subject [OpenLanes::Plans::Subject]
23
+ # @param spec [OpenLanes::Plans::Spec]
24
+ # @return [Boolean]
25
+ def allows?(agent, subject, spec = Plans::Spec.for(subject))
26
+ return false unless agent.serves?(spec.lane)
27
+ return false unless reviewable?(agent, subject)
28
+
29
+ switch = toggled(agent, spec)
30
+ return switch unless switch.nil?
31
+
32
+ agent.needs.all? { |file| units?(subject, file) }
33
+ end
34
+
35
+ # The agents in +agents+ this plan should get, in their given order.
36
+ #
37
+ # {Agent#needs} only ever chooses between agents. When lanes and
38
+ # switches leave one candidate, that one runs: a plan parked at 🎨 is
39
+ # rey-frontend's to finish whatever `plan-frontend.md` says, and an
40
+ # operator who typed `--agent rey-frontend` meant it.
41
+ #
42
+ # @param agents [Array<OpenLanes::Agents::Agent>]
43
+ # @param subject [OpenLanes::Plans::Subject]
44
+ # @return [Array<OpenLanes::Agents::Agent>]
45
+ def filter(agents, subject)
46
+ spec = Plans::Spec.for(subject)
47
+ routed = agents.select do |agent|
48
+ agent.serves?(spec.lane) && toggled(agent, spec) != false && reviewable?(agent, subject)
49
+ end
50
+ return routed if routed.size <= 1
51
+
52
+ routed.select { |agent| toggled(agent, spec) || agent.needs.all? { |file| units?(subject, file) } }
53
+ end
54
+
55
+ # What the spec's switch for this agent says: true forces it, false
56
+ # forbids it, nil leaves the decision to {Agent#needs}. A spec.md that
57
+ # says nothing takes the project's answer from `.openlanes.yml`, so a
58
+ # command-line tool can say once that no plan of it has a front end.
59
+ #
60
+ # @param config [OpenLanes::Config] the project's settings
61
+ # @return [Boolean, nil]
62
+ def toggled(agent, spec, config = OpenLanes::Config.current)
63
+ return nil if agent.toggle.nil?
64
+ return nil unless spec.respond_to?(agent.toggle)
65
+
66
+ value = spec.public_send(agent.toggle)
67
+ return value unless value.nil?
68
+
69
+ config.public_send(agent.toggle) if config.respond_to?(agent.toggle)
70
+ end
71
+
72
+ # A reviewer reviews a pull request, through `gh pr diff`. A plan that
73
+ # reached 🟢 with none open (a shared-tree run, or --no-git-push) would
74
+ # hand it nothing to read, so the plan waits at 🟢 until one exists;
75
+ # `resync prs` records one a human opened.
76
+ #
77
+ # @param agent [OpenLanes::Agents::Agent]
78
+ # @param subject [OpenLanes::Plans::Subject]
79
+ # @return [Boolean]
80
+ def reviewable?(agent, subject) = agent.phase.to_s != "review" || subject.pull_requests.any?(&:open?)
81
+
82
+ # @param subject [OpenLanes::Plans::Subject]
83
+ # @param file [String]
84
+ # @return [Boolean]
85
+ def units?(subject, file) = Plans::Ledger.stripped(subject.read(file)).match?(UNIT_HEADING)
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ # The specialists: their definitions in `agents/*.md`, the registry that
5
+ # loads and filters them, and the roster that reports on them.
6
+ module Agents
7
+ module_function
8
+
9
+ # @param dir [String] where the definitions live
10
+ # @return [OpenLanes::Agents::Registry]
11
+ def registry(dir: Registry::DEFAULT_DIR) = Registry.new(dir:)
12
+
13
+ # @param name [String]
14
+ # @return [OpenLanes::Agents::Agent, nil]
15
+ def find(name, dir: Registry::DEFAULT_DIR) = registry(dir:).find(name)
16
+
17
+ # @return [OpenLanes::Agents::Roster]
18
+ def roster(**) = Roster.new(**)
19
+ end
20
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module CLI
5
+ # `openlanes agents …` — the specialist roster.
6
+ module Agents
7
+ # `openlanes agents describe [NAME]`
8
+ class Describe < Dry::CLI::Command
9
+ include UI
10
+
11
+ desc "Open one specialist's definition in a Markdown viewer"
12
+
13
+ argument :name,
14
+ required: false,
15
+ desc: "Which specialist, full name or a fragment, e.g. leah (default: every one)"
16
+
17
+ option :mdfried,
18
+ type: :boolean,
19
+ default: false,
20
+ aliases: ["-m"],
21
+ desc: "Render in the terminal through mdfried instead of the system viewer"
22
+
23
+ example [
24
+ "leah # leah-researcher, in the system Markdown viewer",
25
+ "luke-backend -m # the same, rendered in this terminal by mdfried",
26
+ " # every agent, one viewer window each"
27
+ ]
28
+
29
+ # The registry instantiates commands bare, so both collaborators
30
+ # default to the real thing; the suite hands in its own to keep
31
+ # viewers from opening on the box running it.
32
+ #
33
+ # @param agents [OpenLanes::Agents::Registry]
34
+ # @param viewer [OpenLanes::Presentation::Viewer]
35
+ def initialize(agents: OpenLanes::Agents.registry, viewer: Presentation.viewer)
36
+ super()
37
+ @agents = agents
38
+ @viewer = viewer
39
+ end
40
+
41
+ # @param name [String, nil]
42
+ # @param mdfried [Boolean]
43
+ # @return [void]
44
+ def call(name: nil, mdfried: false, **_options)
45
+ agents = @agents
46
+ found = name ? agents.match(name) : agents.all
47
+
48
+ if found.empty?
49
+ known = agents.all.map(&:name).join(", ")
50
+ raise Error, name ? "No agent matches #{name}. Known: #{known}" : "No agent definitions in #{agents.dir}"
51
+ end
52
+
53
+ # A fragment that fits several agents is a question, not an order:
54
+ # opening all of them would bury the one that was meant.
55
+ raise Error, "#{name} could be any of #{found.map(&:name).join(", ")} — say which" if name && found.size > 1
56
+
57
+ paths = found.map(&:path)
58
+ mdfried ? @viewer.mdfried(paths) : @viewer.open(paths)
59
+ rescue OpenLanes::Error => e
60
+ error(e.message)
61
+ exit 65
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end