openlanes 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (329) hide show
  1. checksums.yaml +4 -4
  2. data/Brewfile +42 -0
  3. data/agents/bb8-resolver.md +49 -0
  4. data/agents/c3po-operator.md +62 -0
  5. data/agents/chewie-verifier.md +85 -0
  6. data/agents/hansolo-reviewer.md +20 -15
  7. data/agents/lando-broker.md +81 -19
  8. data/agents/leah-researcher.md +6 -6
  9. data/agents/luke-backend.md +41 -20
  10. data/agents/{palpatine-planner.md → luthen-rael-planner.md} +15 -13
  11. data/agents/palpatine-risk-assessor.md +112 -0
  12. data/agents/r2d2-mechanic.md +5 -1
  13. data/agents/rey-frontend.md +38 -19
  14. data/agents/yoda-writer.md +7 -7
  15. data/evals/cases/chewie-verifier/verify-medium/fail/plan/plan.md +7 -0
  16. data/evals/cases/chewie-verifier/verify-medium/fail/plan/pull-requests.md +10 -0
  17. data/evals/cases/chewie-verifier/verify-medium/fail/plan/spec.md +5 -0
  18. data/evals/cases/chewie-verifier/verify-medium/fail/plan/verification/1/final.png +0 -0
  19. data/evals/cases/chewie-verifier/verify-medium/fail/plan/verification/1/report.md +7 -0
  20. data/evals/cases/chewie-verifier/verify-medium/fail/repo/bin/greet +1 -0
  21. data/evals/cases/chewie-verifier/verify-medium/fail/run.json +1 -0
  22. data/evals/cases/chewie-verifier/verify-medium/pass/plan/plan.md +7 -0
  23. data/evals/cases/chewie-verifier/verify-medium/pass/plan/pull-requests.md +10 -0
  24. data/evals/cases/chewie-verifier/verify-medium/pass/plan/spec.md +5 -0
  25. data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/final.png +0 -0
  26. data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/report.md +14 -0
  27. data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/step-01.png +0 -0
  28. data/evals/cases/chewie-verifier/verify-medium/pass/repo/bin/greet +1 -0
  29. data/evals/cases/chewie-verifier/verify-medium/pass/run.json +1 -0
  30. data/evals/cases/chewie-verifier/verify-medium.yml +48 -0
  31. data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/implementation-plan.md +5 -0
  32. data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/plan.md +3 -0
  33. data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/pull-requests.md +12 -0
  34. data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/spec.md +5 -0
  35. data/evals/cases/hansolo-reviewer/coverage-minimum/fail/repo/lib/cart.rb +10 -0
  36. data/evals/cases/hansolo-reviewer/coverage-minimum/fail/repo/spec/cart_spec.rb +9 -0
  37. data/evals/cases/hansolo-reviewer/coverage-minimum/fail/run.json +1 -0
  38. data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/implementation-plan.md +5 -0
  39. data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/plan.md +3 -0
  40. data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/pull-requests.md +15 -0
  41. data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/spec.md +5 -0
  42. data/evals/cases/hansolo-reviewer/coverage-minimum/pass/repo/lib/cart.rb +10 -0
  43. data/evals/cases/hansolo-reviewer/coverage-minimum/pass/repo/spec/cart_spec.rb +9 -0
  44. data/evals/cases/hansolo-reviewer/coverage-minimum/pass/run.json +1 -0
  45. data/evals/cases/hansolo-reviewer/coverage-minimum.yml +76 -0
  46. data/evals/cases/luthen-rael-planner/plan-deep/fail/plan/plan.md +8 -0
  47. data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan.md +2 -2
  48. data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep.yml +1 -1
  49. data/evals/cases/luthen-rael-planner/plan-fast/fail/plan/plan.md +12 -0
  50. data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan.md +2 -2
  51. data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast.yml +1 -1
  52. data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/plan.md +20 -0
  53. data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/pull-requests.md +5 -0
  54. data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/spec.md +5 -0
  55. data/evals/cases/palpatine-risk-assessor/risk-medium/fail/repo/db/migrate/20261020120000_drop_legacy_email.rb +5 -0
  56. data/evals/cases/palpatine-risk-assessor/risk-medium/fail/run.json +1 -0
  57. data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/plan.md +25 -0
  58. data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/pull-requests.md +5 -0
  59. data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/spec.md +5 -0
  60. data/evals/cases/palpatine-risk-assessor/risk-medium/pass/repo/db/migrate/20261020120000_drop_legacy_email.rb +5 -0
  61. data/evals/cases/palpatine-risk-assessor/risk-medium/pass/run.json +1 -0
  62. data/evals/cases/palpatine-risk-assessor/risk-medium.yml +47 -0
  63. data/evals/cases/yoda-writer/spec-medium/pass/plan/spec.md +1 -1
  64. data/lib/openlanes/adapters/base.rb +24 -3
  65. data/lib/openlanes/adapters/capabilities.rb +29 -0
  66. data/lib/openlanes/adapters/claude.rb +32 -10
  67. data/lib/openlanes/adapters/codex.rb +9 -0
  68. data/lib/openlanes/adapters/pi.rb +10 -0
  69. data/lib/openlanes/adapters/stream.rb +145 -1
  70. data/lib/openlanes/adapters.rb +26 -2
  71. data/lib/openlanes/agents/agent.rb +4 -3
  72. data/lib/openlanes/agents/roster.rb +14 -2
  73. data/lib/openlanes/agents/routing.rb +43 -3
  74. data/lib/openlanes/apps/definition.rb +152 -0
  75. data/lib/openlanes/apps/framing.rb +114 -0
  76. data/lib/openlanes/apps/listing.rb +60 -0
  77. data/lib/openlanes/apps/ports.rb +168 -0
  78. data/lib/openlanes/apps/probe.rb +63 -0
  79. data/lib/openlanes/apps/process_spec.rb +39 -0
  80. data/lib/openlanes/apps/runner.rb +174 -0
  81. data/lib/openlanes/apps/status.rb +62 -0
  82. data/lib/openlanes/apps/supervisor.rb +277 -0
  83. data/lib/openlanes/apps/watcher.rb +50 -0
  84. data/lib/openlanes/apps.rb +41 -0
  85. data/lib/openlanes/cli/api/api.rb +10 -0
  86. data/lib/openlanes/cli/api/subcommands/docs.rb +63 -0
  87. data/lib/openlanes/cli/app/app.rb +31 -0
  88. data/lib/openlanes/cli/app/subcommands/start.rb +114 -0
  89. data/lib/openlanes/cli/app/subcommands/status.rb +38 -0
  90. data/lib/openlanes/cli/app/subcommands/stop.rb +44 -0
  91. data/lib/openlanes/cli/base.rb +89 -16
  92. data/lib/openlanes/cli/coverage/coverage.rb +68 -0
  93. data/lib/openlanes/cli/create/create.rb +11 -265
  94. data/lib/openlanes/cli/eval/eval.rb +4 -1
  95. data/lib/openlanes/cli/init/init.rb +39 -2
  96. data/lib/openlanes/cli/linear/subcommands/import.rb +1 -1
  97. data/lib/openlanes/cli/linear/subcommands/projects.rb +1 -1
  98. data/lib/openlanes/cli/list_plans/list_plans.rb +8 -2
  99. data/lib/openlanes/cli/mail/subcommands/ack.rb +52 -0
  100. data/lib/openlanes/cli/mail/subcommands/render.rb +31 -0
  101. data/lib/openlanes/cli/mail/subcommands/send.rb +26 -0
  102. data/lib/openlanes/cli/resync/subcommands/dirs.rb +53 -12
  103. data/lib/openlanes/cli/resync/subcommands/prs.rb +8 -69
  104. data/lib/openlanes/cli/review/review.rb +72 -0
  105. data/lib/openlanes/cli/run/run.rb +22 -454
  106. data/lib/openlanes/cli/setup/setup.rb +51 -0
  107. data/lib/openlanes/cli/start/start.rb +7 -64
  108. data/lib/openlanes/cli/state/state.rb +1 -1
  109. data/lib/openlanes/cli/state/subcommands/resume.rb +61 -0
  110. data/lib/openlanes/cli/state/subcommands/sign.rb +1 -1
  111. data/lib/openlanes/cli/unblock/unblock.rb +11 -205
  112. data/lib/openlanes/cli/web/web.rb +94 -8
  113. data/lib/openlanes/cli/worktree/worktree.rb +1 -1
  114. data/lib/openlanes/cli/worktree/worktree_prune.rb +77 -0
  115. data/lib/openlanes/cli.rb +37 -2
  116. data/lib/openlanes/coverage/cobertura.rb +49 -0
  117. data/lib/openlanes/coverage/config.rb +194 -0
  118. data/lib/openlanes/coverage/istanbul.rb +31 -0
  119. data/lib/openlanes/coverage/lcov.rb +38 -0
  120. data/lib/openlanes/coverage/report.rb +71 -0
  121. data/lib/openlanes/coverage/simplecov.rb +70 -0
  122. data/lib/openlanes/coverage.rb +33 -0
  123. data/lib/openlanes/engine/agent_locks.rb +231 -0
  124. data/lib/openlanes/engine/arbitration.rb +25 -0
  125. data/lib/openlanes/engine/board.rb +36 -5
  126. data/lib/openlanes/engine/conversions.rb +160 -0
  127. data/lib/openlanes/engine/coverage_gate.rb +217 -0
  128. data/lib/openlanes/engine/dispatcher.rb +1229 -116
  129. data/lib/openlanes/engine/event_journal/replay.rb +96 -0
  130. data/lib/openlanes/engine/event_journal.rb +148 -0
  131. data/lib/openlanes/engine/extensions.rb +271 -0
  132. data/lib/openlanes/engine/merging.rb +459 -0
  133. data/lib/openlanes/engine/narrator.rb +107 -0
  134. data/lib/openlanes/engine/plan_locks.rb +114 -0
  135. data/lib/openlanes/engine/pr_outcomes.rb +161 -0
  136. data/lib/openlanes/engine/pr_watch.rb +83 -0
  137. data/lib/openlanes/engine/progress_log.rb +38 -105
  138. data/lib/openlanes/engine/quality_question.rb +125 -0
  139. data/lib/openlanes/engine/rebase.rb +200 -0
  140. data/lib/openlanes/engine/run_workflow.rb +776 -0
  141. data/lib/openlanes/engine/runner.rb +186 -15
  142. data/lib/openlanes/engine/start_workflow.rb +77 -0
  143. data/lib/openlanes/engine/state_file.rb +101 -34
  144. data/lib/openlanes/engine/tally.rb +1 -1
  145. data/lib/openlanes/engine/verification/app_session.rb +104 -0
  146. data/lib/openlanes/engine/verification/driver.rb +314 -0
  147. data/lib/openlanes/engine/verification/outcome.rb +516 -0
  148. data/lib/openlanes/engine/verification/report.rb +95 -0
  149. data/lib/openlanes/engine/verification/round.rb +100 -0
  150. data/lib/openlanes/engine/verification/screenshots.rb +120 -0
  151. data/lib/openlanes/engine/verification.rb +57 -0
  152. data/lib/openlanes/engine.rb +30 -1
  153. data/lib/openlanes/execution/boundary.rb +212 -0
  154. data/lib/openlanes/execution/child.rb +4 -2
  155. data/lib/openlanes/execution/clock.rb +36 -6
  156. data/lib/openlanes/execution/control.rb +32 -0
  157. data/lib/openlanes/execution/executor.rb +114 -35
  158. data/lib/openlanes/execution/process_start.rb +63 -0
  159. data/lib/openlanes/execution/prompt.rb +236 -34
  160. data/lib/openlanes/execution/transcript.rb +159 -8
  161. data/lib/openlanes/execution.rb +21 -0
  162. data/lib/openlanes/lifecycle/adoption.rb +25 -8
  163. data/lib/openlanes/lifecycle/aging.rb +166 -0
  164. data/lib/openlanes/lifecycle/allocator.rb +216 -0
  165. data/lib/openlanes/lifecycle/brief.rb +32 -2
  166. data/lib/openlanes/lifecycle/completion.rb +12 -6
  167. data/lib/openlanes/lifecycle/create_workflow.rb +272 -0
  168. data/lib/openlanes/lifecycle/creator.rb +39 -11
  169. data/lib/openlanes/lifecycle/lanes.rb +1 -1
  170. data/lib/openlanes/lifecycle/resync.rb +170 -42
  171. data/lib/openlanes/lifecycle/unblock_workflow.rb +234 -0
  172. data/lib/openlanes/lifecycle/unblocker.rb +76 -2
  173. data/lib/openlanes/lifecycle.rb +37 -1
  174. data/lib/openlanes/linear/mapping.rb +4 -1
  175. data/lib/openlanes/linear/unit.rb +1 -1
  176. data/lib/openlanes/live/chat.rb +665 -0
  177. data/lib/openlanes/live/choice_converter.rb +166 -0
  178. data/lib/openlanes/live/choice_judge.rb +109 -0
  179. data/lib/openlanes/live/claims.rb +9 -1
  180. data/lib/openlanes/live/command.rb +13 -10
  181. data/lib/openlanes/live/commands.rb +221 -31
  182. data/lib/openlanes/live/copilot/brief.rb +128 -0
  183. data/lib/openlanes/live/copilot/cards.rb +226 -0
  184. data/lib/openlanes/live/copilot/operator.rb +259 -0
  185. data/lib/openlanes/live/copilot/slash.rb +82 -0
  186. data/lib/openlanes/live/copilot.rb +290 -0
  187. data/lib/openlanes/live/event_bus/base.rb +52 -0
  188. data/lib/openlanes/live/event_bus/memory_bus.rb +14 -0
  189. data/lib/openlanes/live/event_bus/redis_bus.rb +10 -0
  190. data/lib/openlanes/live/event_bus/transaction.rb +27 -0
  191. data/lib/openlanes/live/event_bus.rb +29 -0
  192. data/lib/openlanes/live/interviewer.rb +146 -0
  193. data/lib/openlanes/live/keys.rb +114 -0
  194. data/lib/openlanes/live/mail.rb +143 -0
  195. data/lib/openlanes/live/memory_store.rb +36 -1
  196. data/lib/openlanes/live/memory_subscriber.rb +40 -0
  197. data/lib/openlanes/live/news.rb +61 -0
  198. data/lib/openlanes/live/node.rb +20 -0
  199. data/lib/openlanes/live/outage.rb +225 -0
  200. data/lib/openlanes/live/outage_report.rb +89 -0
  201. data/lib/openlanes/live/publisher.rb +42 -2
  202. data/lib/openlanes/live/run_record.rb +39 -1
  203. data/lib/openlanes/live/run_state.rb +19 -2
  204. data/lib/openlanes/live/session.rb +38 -4
  205. data/lib/openlanes/live/snapshot.rb +103 -42
  206. data/lib/openlanes/live/stations.rb +12 -8
  207. data/lib/openlanes/live/store.rb +36 -1
  208. data/lib/openlanes/live/subscriber.rb +93 -0
  209. data/lib/openlanes/live.rb +71 -1
  210. data/lib/openlanes/plans/blocked_file.rb +117 -0
  211. data/lib/openlanes/plans/choices.rb +228 -0
  212. data/lib/openlanes/plans/dependencies.rb +304 -0
  213. data/lib/openlanes/plans/feature.rb +75 -16
  214. data/lib/openlanes/plans/journal.rb +169 -0
  215. data/lib/openlanes/plans/last_known_state.rb +82 -0
  216. data/lib/openlanes/plans/lock.rb +171 -0
  217. data/lib/openlanes/plans/mailbox.rb +145 -4
  218. data/lib/openlanes/plans/plan_state.rb +590 -9
  219. data/lib/openlanes/plans/pull_request_machine.rb +163 -0
  220. data/lib/openlanes/plans/review_label.rb +139 -0
  221. data/lib/openlanes/plans/spec.rb +239 -3
  222. data/lib/openlanes/plans/state_machine.rb +63 -30
  223. data/lib/openlanes/plans/status.rb +60 -11
  224. data/lib/openlanes/plans/subject.rb +4 -0
  225. data/lib/openlanes/plans/tree.rb +101 -10
  226. data/lib/openlanes/plans/verdict.rb +124 -0
  227. data/lib/openlanes/plans.rb +19 -0
  228. data/lib/openlanes/presentation/api_docs.rb +76 -0
  229. data/lib/openlanes/presentation/documentation.rb +105 -12
  230. data/lib/openlanes/presentation/pr_changes.rb +90 -0
  231. data/lib/openlanes/presentation/reporter.rb +66 -7
  232. data/lib/openlanes/presentation.rb +20 -1
  233. data/lib/openlanes/setup/check.rb +16 -0
  234. data/lib/openlanes/setup/machine.rb +179 -0
  235. data/lib/openlanes/setup.rb +34 -0
  236. data/lib/openlanes/support/clock.rb +57 -0
  237. data/lib/openlanes/support/config.rb +318 -6
  238. data/lib/openlanes/support/log.rb +206 -0
  239. data/lib/openlanes/support/refusal.rb +15 -0
  240. data/lib/openlanes/support/runtime.rb +61 -0
  241. data/lib/openlanes/support/spawner.rb +51 -0
  242. data/lib/openlanes/support/ui.rb +110 -21
  243. data/lib/openlanes/vcs/ci_gate.rb +31 -0
  244. data/lib/openlanes/vcs/github.rb +362 -12
  245. data/lib/openlanes/vcs/label_migration.rb +132 -0
  246. data/lib/openlanes/vcs/label_sync.rb +75 -0
  247. data/lib/openlanes/vcs/publisher.rb +287 -7
  248. data/lib/openlanes/vcs/refresh.rb +337 -0
  249. data/lib/openlanes/vcs/rename_date.rb +83 -0
  250. data/lib/openlanes/vcs/review.rb +66 -0
  251. data/lib/openlanes/vcs/worktree.rb +210 -14
  252. data/lib/openlanes/vcs.rb +18 -0
  253. data/lib/openlanes/version.rb +1 -1
  254. data/lib/openlanes/web/admin_auth.rb +104 -0
  255. data/lib/openlanes/web/app.rb +213 -15
  256. data/lib/openlanes/web/context.rb +33 -9
  257. data/lib/openlanes/web/demo.rb +39 -14
  258. data/lib/openlanes/web/demo_board.rb +63 -5
  259. data/lib/openlanes/web/events.rb +137 -9
  260. data/lib/openlanes/web/launcher.rb +70 -8
  261. data/lib/openlanes/web/listen.rb +133 -0
  262. data/lib/openlanes/web/plan_files.rb +136 -0
  263. data/lib/openlanes/web/request_log.rb +108 -0
  264. data/lib/openlanes/web/server.rb +31 -10
  265. data/lib/openlanes/web/stores.rb +11 -0
  266. data/lib/openlanes/web.rb +19 -4
  267. data/lib/openlanes.rb +45 -7
  268. data/openlanes.gemspec +9 -4
  269. data/schemas/journal-event.schema.json +72 -0
  270. data/schemas/plan-state.schema.json +163 -2
  271. data/skills/openlanes/SKILL.md +2 -2
  272. data/skills/openlanes-documents/SKILL.md +4 -4
  273. data/skills/openlanes-documents/references/blocked-template.md +2 -2
  274. data/skills/openlanes-documents/references/closing-templates.md +1 -1
  275. data/skills/openlanes-documents/references/plan-template.md +12 -4
  276. data/skills/openlanes-documents/references/retroactive-spec.md +1 -1
  277. data/skills/openlanes-run/SKILL.md +5 -5
  278. data/skills/openlanes-spec/SKILL.md +1 -1
  279. data/skills/openlanes-status/SKILL.md +1 -1
  280. data/web/dist/assets/favicon-BFJEKKAE.png +0 -0
  281. data/web/dist/assets/{index-Bp7W-ZTq.js → index-690ayLS4.js} +2 -2
  282. data/web/dist/assets/index-6ZmW4slI.js +199 -0
  283. data/web/dist/assets/{index-Byyxbh_z.js → index-B5KzlXp1.js} +2 -2
  284. data/web/dist/assets/{index-8dNhRmiO.js → index-BAo_v9bU.js} +2 -2
  285. data/web/dist/assets/{index-2xPC-eHt.js → index-BDm7g-xG.js} +2 -2
  286. data/web/dist/assets/{index-D0rmJN3h.js → index-BFH9bC7v.js} +2 -2
  287. data/web/dist/assets/{index-OR-odsl9.js → index-BVHPV7z4.js} +2 -2
  288. data/web/dist/assets/{index-C0VmkrO-.js → index-B_CPqjnA.js} +2 -2
  289. data/web/dist/assets/{index-DDI7zoAS.js → index-BhwBH5ct.js} +2 -2
  290. data/web/dist/assets/{index-BdR34cXC.js → index-Bq5AdwEt.js} +2 -2
  291. data/web/dist/assets/{index-uM88Njdj.js → index-CAW6ClM2.js} +2 -2
  292. data/web/dist/assets/index-Cp86u27u.css +1 -0
  293. data/web/dist/assets/{index-P3vSkiU-.js → index-Culm4pMO.js} +2 -2
  294. data/web/dist/assets/{index-DGLUuEXi.js → index-DElDJfp1.js} +2 -2
  295. data/web/dist/assets/{index-CYxcbpRM.js → index-Dja7osNW.js} +2 -2
  296. data/web/dist/assets/{index-popk9wbW.js → index-DjhU3FzU.js} +2 -2
  297. data/web/dist/assets/{index-mRV4XhJZ.js → index-Dutbxj4o.js} +2 -2
  298. data/web/dist/assets/{index-DB4QwuYn.js → index-RkDhPMDt.js} +2 -2
  299. data/web/dist/assets/{index-B7-O9ujQ.js → index-_mptZjlC.js} +2 -2
  300. data/web/dist/assets/logo-DXDCUiD8.png +0 -0
  301. data/web/dist/index.html +3 -3
  302. data/web/src/api/fixtures/board.json +203 -28
  303. metadata +1053 -881
  304. data/bin/factory-e2e +0 -157
  305. data/bin/plan-number +0 -164
  306. data/bin/setup +0 -26
  307. data/evals/cases/palpatine-planner/plan-deep/fail/plan/plan.md +0 -8
  308. data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan.md +0 -12
  309. data/web/dist/assets/favicon-C8pAuqqr.png +0 -0
  310. data/web/dist/assets/index-CI74IkWP.js +0 -198
  311. data/web/dist/assets/index-a4EHR-u4.css +0 -1
  312. data/web/dist/assets/logo-on-dark-DI9gCdN4.png +0 -0
  313. data/web/dist/assets/logo-on-light-BciwqxcJ.png +0 -0
  314. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/plan/plan-backend.md +0 -0
  315. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/plan/spec.md +0 -0
  316. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/run.json +0 -0
  317. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan-backend.md +0 -0
  318. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan-frontend.md +0 -0
  319. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/spec.md +0 -0
  320. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/run.json +0 -0
  321. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/plan-backend.md +0 -0
  322. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/plan-frontend.md +0 -0
  323. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/spec.md +0 -0
  324. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/run.json +0 -0
  325. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan-backend.md +0 -0
  326. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan-frontend.md +0 -0
  327. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/spec.md +0 -0
  328. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/run.json +0 -0
  329. /data/exe/{depot → ol} +0 -0
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenLanes
4
+ module Live
5
+ # Rewrites a question an agent wrote as prose into D3's shape (D9): a
6
+ # short context paragraph and lettered options, exactly one recommended.
7
+ # The page draws a button per option and a text box under them, so a
8
+ # person can answer with one click instead of composing a reply to a
9
+ # paragraph.
10
+ #
11
+ # One call to a small model on OpenRouter, through `inquirex-llm`'s
12
+ # `OpenRouterAdapter`, which asks for a JSON object and checks that both
13
+ # fields came back. What the model returns is not trusted to be D3: it is
14
+ # cleaned, written out with {Plans::Choices.write}, and read back with
15
+ # {Plans::Choices.parse}, and anything that does not survive the trip is
16
+ # {Invalid}.
17
+ #
18
+ # It neither reads nor writes `blocked.md`; {Engine::Conversions} does,
19
+ # so this stays a function of text, and a seam: the suite sets
20
+ # {Live.choice_converter} to nil, and examples about it hand it an
21
+ # adapter double, so nothing in the tests reaches OpenRouter.
22
+ class ChoiceConverter
23
+ # Small and quick, and listed by OpenRouter and by inquirex-llm's own
24
+ # catalogue (`Models::OPENROUTER_ALIASES[:claude_haiku]`).
25
+ # `questions.model` in `.openlanes.yml` replaces it.
26
+ DEFAULT_MODEL = "anthropic/claude-haiku-5.5"
27
+
28
+ # What the model reads with the question.
29
+ PROMPT = <<~PROMPT
30
+ A software agent stopped and wrote the question below for a person. The person answers it in a
31
+ web page that shows one button per option and a free-text box under them. Rewrite the question as
32
+ multiple choice:
33
+
34
+ - `context`: one short paragraph with what the person needs to know to choose. Keep every fact,
35
+ number and name that matters; drop repetition. Do not repeat the heading.
36
+ - `options`: an array of 2 to 5 objects `{"label": String, "recommended": Boolean}`, each a distinct
37
+ answer the question already implies, written as one short line in the agent's own terms.
38
+ Exactly one has `"recommended": true`: the one the question itself prefers, or, if it prefers
39
+ none, the safest one.
40
+
41
+ Never change what is being asked, never add facts the question does not state, never answer it,
42
+ and never add an option for "something else" or "type something instead": the page has a text box.
43
+ PROMPT
44
+
45
+ # What the model answered could not be made into a D3 question.
46
+ class Invalid < Error; end
47
+
48
+ # @!attribute [r] body
49
+ # @return [String] the question's new body, D3-shaped Markdown
50
+ # @!attribute [r] count
51
+ # @return [Integer] how many options it offers
52
+ # @!attribute [r] model
53
+ # @return [String] the model that wrote it
54
+ Result = Data.define(:body, :count, :model)
55
+
56
+ # A converter when OPENROUTER_API_KEY is set, nil otherwise.
57
+ #
58
+ # @param env [#[]]
59
+ # @param config [OpenLanes::Config]
60
+ # @return [OpenLanes::Live::ChoiceConverter, nil]
61
+ def self.from_env(env = ENV, config: Config.current)
62
+ key = env["OPENROUTER_API_KEY"].to_s.strip
63
+ key.empty? ? nil : new(api_key: key, model: config.questions_model || DEFAULT_MODEL)
64
+ end
65
+
66
+ # The model's answer, cleaned and written in D3's shape.
67
+ #
68
+ # @param output [Hash] `context` and `options`, as the adapter parsed them
69
+ # @return [String] the new body
70
+ # @raise [Invalid] when it cannot be one
71
+ def self.render(output)
72
+ output = output.to_h.transform_keys(&:to_sym)
73
+ context = output[:context].to_s.strip
74
+ raise Invalid, "no context" if context.empty?
75
+ # A heading inside the body would end the question there, and a
76
+ # `Bn` or `An` line would start another one.
77
+ if [Plans::BlockedFile::HEADING, Plans::OPEN_BLOCK, Plans::ANSWER_BLOCK].any? { context.match?(it) }
78
+ raise Invalid, "the context holds a heading"
79
+ end
80
+
81
+ options = Array(output[:options]).map { option(it) }
82
+ .reject { |label, _| label.match?(Plans::Choices::SOMETHING_ELSE) }
83
+ raise Invalid, "#{options.size} option#{"s" unless options.size == 1}, not 2 or more" if options.size < 2
84
+
85
+ recommended = options.each_index.select { options[it].last }
86
+ raise Invalid, "#{recommended.size} options recommended, not one" unless recommended.size == 1
87
+
88
+ body = Plans::Choices.write(context:, labels: options.map(&:first), recommended: recommended.first)
89
+ back = Plans::Choices.parse(body)
90
+ raise Invalid, "the rewrite does not read back as #{options.size} choices" unless back&.options&.size == options.size
91
+
92
+ body
93
+ end
94
+
95
+ # One option as a label on one line and whether it is recommended; a
96
+ # recommendation the model wrote into the label is taken out of it, since
97
+ # the flag already says so.
98
+ #
99
+ # @param raw [Hash, Object]
100
+ # @return [Array(String, Boolean)]
101
+ # @raise [Invalid]
102
+ def self.option(raw)
103
+ raise Invalid, "an option is not an object" unless raw.is_a?(Hash)
104
+
105
+ raw = raw.transform_keys(&:to_sym)
106
+ label = raw[:label].to_s.gsub(/\s+/, " ").sub(Plans::Choices::RECOMMENDED, " ").strip
107
+ raise Invalid, "an option has no label" if label.empty?
108
+
109
+ [label, [true, "true"].include?(raw[:recommended])]
110
+ end
111
+ private_class_method :option
112
+
113
+ # @param api_key [String, nil] OpenRouter's; only read to build the default adapter
114
+ # @param model [String] an OpenRouter model id
115
+ # @param adapter [#call, nil] answers `call(node, answers)` with a Hash
116
+ # holding `context` and `options`, as `Inquirex::LLM::OpenRouterAdapter`
117
+ # does; built on first use when nil, so a process that never converts
118
+ # never loads inquirex
119
+ def initialize(api_key: nil, model: DEFAULT_MODEL, adapter: nil)
120
+ @api_key = api_key
121
+ @model = model
122
+ @adapter = adapter
123
+ @mutex = Mutex.new
124
+ end
125
+
126
+ # @return [String]
127
+ attr_reader :model
128
+
129
+ # Blocks for as long as the request takes (the adapter gives up after
130
+ # 30 seconds without an answer), so the engine calls it on a thread.
131
+ #
132
+ # @param heading [String] the question's heading, for context; it is kept
133
+ # @param body [String] the question as the agent wrote it
134
+ # @return [OpenLanes::Live::ChoiceConverter::Result]
135
+ # @raise [Invalid] when the answer is not a D3 question
136
+ # @raise [StandardError] whatever the adapter raises: no network, a 4xx, bad JSON
137
+ def call(heading:, body:)
138
+ output = adapter.call(node, { heading: heading.to_s, question: body.to_s })
139
+ written = self.class.render(output)
140
+ Result.new(body: written, count: Plans::Choices.parse(written).options.size, model:)
141
+ end
142
+
143
+ private
144
+
145
+ def adapter
146
+ @mutex.synchronize do
147
+ @adapter ||= begin
148
+ require "inquirex-llm"
149
+ Inquirex::LLM::OpenRouterAdapter.new(api_key: @api_key, model:)
150
+ end
151
+ end
152
+ end
153
+
154
+ def node
155
+ @node ||= begin
156
+ require "inquirex-llm"
157
+ Inquirex::LLM::Node.new(id: :choices,
158
+ verb: :extract,
159
+ prompt: PROMPT,
160
+ schema: Inquirex::LLM::Schema.new(context: :string, options: :array),
161
+ from_steps: %i[heading question])
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module OpenLanes
6
+ module Live
7
+ # A second opinion on {Plans::Choices}: does this message ask its reader
8
+ # to choose one of the options it lists? Asked of Jev, TypeSafe's System
9
+ # One model, through `inquirex-llm`'s `JevAdapter`, as one boolean.
10
+ #
11
+ # The parser cannot tell "Done so far: 1. … 2. …" from a numbered choice;
12
+ # Jev can. Its answer only ever narrows: `false` takes the buttons away and
13
+ # leaves the text box, `true` keeps them, and `nil` (no answer, an error, a
14
+ # timeout, an answer too unsure to count) leaves the parser's verdict as it
15
+ # was. A missing key therefore costs nothing but the second opinion.
16
+ #
17
+ # The snapshot is rebuilt every time the board moves, so every answer is
18
+ # kept, in this process, by the SHA256 of the text: one request per
19
+ # distinct question, ever. A request still in flight when {#call} gives up
20
+ # waiting goes on in its thread, and the next snapshot finds its answer.
21
+ #
22
+ # A seam, like {Vcs::GitHub} and {Linear::API}: the suite sets
23
+ # {Live.choice_judge} to nil, and examples about the judge hand it an
24
+ # adapter double, so nothing in the tests reaches TypeSafe.
25
+ class ChoiceJudge
26
+ # What Jev reads with the message. Its boolean question is whether
27
+ # `asks_to_choose` is true according to the message.
28
+ PROMPT = <<~PROMPT
29
+ The message below was written by a software agent to a person, who answers it in a web page.
30
+ `asks_to_choose` is true when the message asks the person to pick one of the options it lists
31
+ (lettered or numbered alternatives, or a plain yes/no question), and false when the list is
32
+ something else: steps already done, findings, a plan, or a question that wants a written answer.
33
+ PROMPT
34
+
35
+ # How long a snapshot waits for an answer it does not have yet.
36
+ TIMEOUT = 3.0
37
+
38
+ # Below this Jev is guessing, and the parser's verdict stands.
39
+ MIN_CONFIDENCE = 0.6
40
+
41
+ # The schema field Jev answers.
42
+ FIELD = :asks_to_choose
43
+
44
+ # A judge when TYPESAFE_API_KEY is set, nil otherwise.
45
+ #
46
+ # @param env [#[]]
47
+ # @return [OpenLanes::Live::ChoiceJudge, nil]
48
+ def self.from_env(env = ENV)
49
+ key = env["TYPESAFE_API_KEY"].to_s.strip
50
+ key.empty? ? nil : new(api_key: key)
51
+ end
52
+
53
+ # @param api_key [String, nil] TypeSafe's; only read to build the default adapter
54
+ # @param adapter [#call, nil] answers `call(node, answers)` with a Hash
55
+ # holding {FIELD}, as `Inquirex::LLM::JevAdapter` does; built on first
56
+ # use when nil, so a process that never judges never loads inquirex
57
+ # @param timeout [Float] seconds {#call} waits for an answer it lacks
58
+ def initialize(api_key: nil, adapter: nil, timeout: TIMEOUT)
59
+ @api_key = api_key
60
+ @adapter = adapter
61
+ @timeout = timeout
62
+ @answers = {}
63
+ @mutex = Mutex.new
64
+ end
65
+
66
+ # @param text [String] the question, as the person would read it
67
+ # @return [Boolean, nil] whether it asks to choose; nil when Jev has not
68
+ # said, or could not
69
+ def call(text)
70
+ key = Digest::SHA256.hexdigest(text.to_s)
71
+ asking = @mutex.synchronize { @answers[key] ||= ask(text.to_s) }
72
+ asking.join(@timeout) ? asking.value : nil
73
+ end
74
+
75
+ private
76
+
77
+ # @return [Thread] whose value is true, false or nil
78
+ def ask(text)
79
+ Thread.new do
80
+ Thread.current.report_on_exception = false
81
+ adapter.call(node, { message: text })[FIELD]
82
+ rescue StandardError => e
83
+ UI.log("choice judge: no verdict (#{e.class})")
84
+ nil
85
+ end
86
+ end
87
+
88
+ def adapter
89
+ @mutex.synchronize do
90
+ @adapter ||= begin
91
+ require "inquirex-llm"
92
+ Inquirex::LLM::JevAdapter.new(api_key: @api_key, min_confidence: MIN_CONFIDENCE)
93
+ end
94
+ end
95
+ end
96
+
97
+ def node
98
+ @node ||= begin
99
+ require "inquirex-llm"
100
+ Inquirex::LLM::Node.new(id: :choice,
101
+ verb: :extract,
102
+ prompt: PROMPT,
103
+ schema: Inquirex::LLM::Schema.new(FIELD => :boolean),
104
+ from_steps: [:message])
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -39,9 +39,17 @@ module OpenLanes
39
39
  # Extend every claim this node holds. One that lapsed and was taken by
40
40
  # another node in the meantime is dropped from {#held}, not fought for.
41
41
  #
42
+ # One that is simply gone is taken back: a Redis that restarted empty
43
+ # (W32) has lost every claim, and reading that as "another machine
44
+ # took it" stopped this machine's own agents for plans nobody else
45
+ # wanted. `SET NX` still loses to a node that got there first.
46
+ #
42
47
  # @return [Array<String>] the ordinals lost
43
48
  def renew
44
- lost = held.reject { |ordinal| @store.compare_and_expire(@keys.claim(ordinal), @keys.node, @ttl) }
49
+ lost = held.reject do |ordinal|
50
+ key = @keys.claim(ordinal)
51
+ @store.compare_and_expire(key, @keys.node, @ttl) || @store.set(key, @keys.node, ex: @ttl, nx: true)
52
+ end
45
53
  @mutex.synchronize { @held -= lost }
46
54
  lost
47
55
  end
@@ -42,25 +42,28 @@ module OpenLanes
42
42
 
43
43
  # Every kind the page may send.
44
44
  KINDS = %w[answer hold_blocked release_hold resume discard message hold wrap_up compact stop stop_all
45
- set_model switch_model add_plan create_plans].freeze
45
+ set_model switch_model add_plan create_plans respond pause_discuss resume_anyway
46
+ stop_plan restart_plan set_concurrency].freeze
46
47
 
47
48
  # Kinds that act on one plan, and so must name it.
48
49
  PLAN_KINDS = %w[answer hold_blocked release_hold resume discard message hold wrap_up compact stop
49
- set_model switch_model].freeze
50
+ set_model switch_model respond pause_discuss resume_anyway stop_plan restart_plan].freeze
50
51
 
51
52
  # Kinds that act on one agent of that plan, and so must name it too.
52
53
  AGENT_KINDS = %w[hold wrap_up compact stop set_model switch_model].freeze
53
54
 
54
55
  # Payload fields each kind cannot do without.
55
56
  PAYLOAD = {
56
- "answer" => %w[question text],
57
- "hold_blocked" => %w[question],
58
- "release_hold" => %w[question],
59
- "message" => %w[text],
60
- "set_model" => %w[model],
61
- "switch_model" => %w[model],
62
- "add_plan" => %w[ordinal],
63
- "create_plans" => %w[idea]
57
+ "answer" => %w[question text],
58
+ "hold_blocked" => %w[question],
59
+ "release_hold" => %w[question],
60
+ "message" => %w[text],
61
+ "set_model" => %w[model],
62
+ "switch_model" => %w[model],
63
+ "add_plan" => %w[ordinal],
64
+ "create_plans" => %w[idea],
65
+ "respond" => %w[text],
66
+ "set_concurrency" => %w[limit]
64
67
  }.freeze
65
68
 
66
69
  # Ids are ULIDs; this only keeps an absurd one out of a hash field.