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
@@ -37,6 +37,14 @@ module OpenLanes
37
37
  # compacts its session from outside (see {Executor::Handle#compact!}).
38
38
  WORDS = { "hold" => HOLD, "wrap_up" => WRAP_UP, "stop" => STOP }.freeze
39
39
 
40
+ # Redis stopped answering (W32): every agent stops at its next safe
41
+ # point and waits until the file says anything else. Its clock stands
42
+ # still meanwhile, so the wait costs it no time.
43
+ PAUSE = "PAUSE"
44
+
45
+ # Redis is back: carry on from where the PAUSE found you.
46
+ RESUME = "RESUME"
47
+
40
48
  # Seconds between {#quit!} and the harness terminating whatever is still
41
49
  # running. Long enough to write files and a handoff note; short enough
42
50
  # that q means quit rather than "quit eventually".
@@ -46,6 +54,7 @@ module OpenLanes
46
54
  @files = []
47
55
  @quit = false
48
56
  @interrupted = false
57
+ @paused = false
49
58
  @deadline = nil
50
59
 
51
60
  class << self
@@ -107,6 +116,9 @@ module OpenLanes
107
116
  def quit!
108
117
  stop!
109
118
  @mutex.synchronize {
119
+ # Quitting overrides a pause: the clocks run again, so an agent
120
+ # that does not stop is still stopped.
121
+ @paused = false
110
122
  @quit = true
111
123
  @deadline ||= UI.monotonic + GRACE
112
124
  }
@@ -127,6 +139,25 @@ module OpenLanes
127
139
  }
128
140
  end
129
141
 
142
+ # Every running agent waits where it is (W32).
143
+ #
144
+ # @return [void]
145
+ def pause!
146
+ @mutex.synchronize { @paused = true }
147
+ broadcast(PAUSE)
148
+ end
149
+
150
+ # Every paused agent carries on.
151
+ #
152
+ # @return [void]
153
+ def resume!
154
+ @mutex.synchronize { @paused = false }
155
+ broadcast(RESUME)
156
+ end
157
+
158
+ # @return [Boolean] whether agents are paused; their clocks stand still
159
+ def paused? = @mutex.synchronize { @paused }
160
+
130
161
  # @return [Boolean] whether `q` has been pressed
131
162
  def quit? = @mutex.synchronize { @quit }
132
163
 
@@ -172,6 +203,7 @@ module OpenLanes
172
203
  @files.each { |f| FileUtils.rm_f(f) }
173
204
  @files.clear
174
205
  @quit = false
206
+ @paused = false
175
207
  @interrupted = false
176
208
  @deadline = nil
177
209
  }
@@ -15,9 +15,10 @@ module OpenLanes
15
15
  #
16
16
  # BEFORE — the agent is told, and `--disallowedTools` withholds the tools
17
17
  # that would let it push.
18
- # AFTER — the harness checks that HEAD did not move and no new remote ref
19
- # appeared. A prompt is a request; a check is a guarantee, and only
20
- # one of them survives a model deciding it knows better.
18
+ # AFTER — {Boundary} checks that HEAD did not move and that no
19
+ # remote-tracking ref was moved by a push. A prompt is a request;
20
+ # a check is a guarantee, and only one of them survives a model
21
+ # deciding it knows better.
21
22
  class Executor
22
23
  # What one invocation did, and what it spent doing it.
23
24
  #
@@ -124,12 +125,30 @@ module OpenLanes
124
125
  # @return [String, nil] the CLI's session id, once its stream named it
125
126
  attr_accessor :session
126
127
 
128
+ # @return [String, nil] the raw NDJSON trace the agent's stream is
129
+ # copied into, so the engine journal can name it and its offset
130
+ attr_accessor :trace
131
+
127
132
  # @return [OpenLanes::Execution::Executor::Compaction, nil] the latest
128
133
  # compaction asked for
129
134
  def compaction = @mutex.synchronize { @compaction }
130
135
 
131
136
  # @return [OpenLanes::Execution::Clock, nil]
132
- attr_accessor :clock
137
+ attr_reader :clock
138
+
139
+ # The clock, once {Executor#call} has started it. Seconds granted
140
+ # before then (a round given more time, E1) go onto it now rather
141
+ # than being dropped for want of a clock.
142
+ #
143
+ # @param clock [OpenLanes::Execution::Clock, nil]
144
+ # @return [void]
145
+ def clock=(clock)
146
+ @mutex.synchronize do
147
+ @clock = clock
148
+ clock&.extend!(@pending) if @pending&.positive?
149
+ @pending = nil
150
+ end
151
+ end
133
152
 
134
153
  # @return [String, nil]
135
154
  attr_reader :control
@@ -184,9 +203,18 @@ module OpenLanes
184
203
  child&.terminate
185
204
  end
186
205
 
206
+ # More time on the clock, or on the clock to come.
207
+ #
187
208
  # @param seconds [Integer]
188
- # @return [void]
189
- def extend!(seconds) = clock&.extend!(seconds)
209
+ # @return [Boolean] false when the clock had already stopped the agent
210
+ def extend!(seconds)
211
+ @mutex.synchronize do
212
+ next @clock.extend!(seconds) if @clock
213
+
214
+ @pending = @pending.to_i + seconds
215
+ true
216
+ end
217
+ end
190
218
 
191
219
  # Ask for the session to be compacted (W28): INTERRUPT, so the agent
192
220
  # leaves a resume note and ends its turn, and SIGKILL after +grace+
@@ -290,7 +318,10 @@ module OpenLanes
290
318
  def tools = @all.sum(&:tools)
291
319
 
292
320
  # What the sessions already over spent, added to the running one's
293
- # progress so the meter carries on rather than starting again.
321
+ # progress so the meter carries on rather than starting again. Their
322
+ # journal lines go first, for the same reason: the dispatcher counts
323
+ # its place in the list, and a list that started again at the
324
+ # continuation would have it skip the continuation's first entries.
294
325
  #
295
326
  # @param progress [OpenLanes::Execution::Transcript::Progress]
296
327
  # @return [OpenLanes::Execution::Transcript::Progress]
@@ -298,7 +329,8 @@ module OpenLanes
298
329
  before = @all[0...-1]
299
330
  progress.with(up: progress.up + before.sum(&:up),
300
331
  down: progress.down + before.sum(&:down),
301
- subagents: progress.subagents + before.sum(&:spawned))
332
+ subagents: progress.subagents + before.sum(&:spawned),
333
+ journal: (before.flat_map { it.respond_to?(:journal) ? it.journal : [] } + progress.journal).freeze)
302
334
  end
303
335
  end
304
336
 
@@ -321,8 +353,13 @@ module OpenLanes
321
353
  # as a regular expression that nothing referenced — a guard in the shape of
322
354
  # a constant, enforcing nothing — which is exactly how `gh pr review` came
323
355
  # to be reachable by an agent nobody had granted it to.
356
+ #
357
+ # `git rebase` is here because the harness owns the rebase onto the
358
+ # default branch ({Vcs::Refresh}): an agent resolving a stopped one only
359
+ # edits and stages, and a `--continue`, `--skip` or `--abort` of its own
360
+ # would move HEAD inside the window the after-check guards.
324
361
  FORBIDDEN_COMMANDS = [
325
- "git push", "git commit",
362
+ "git push", "git commit", "git rebase",
326
363
  "gh pr create", "gh pr edit", "gh pr merge",
327
364
  "gh pr review", "gh pr comment",
328
365
  "gh release create"
@@ -335,7 +372,11 @@ module OpenLanes
335
372
  # does not: an approval is reversible, visible, and attributable to the
336
373
  # identity that made it. That difference is the whole line between
337
374
  # `hansolo-reviewer` approving and `hansolo-reviewer` merging.
338
- UNGRANTABLE = ["git push", "gh pr merge"].freeze
375
+ #
376
+ # A rebase is the harness's alone for the same reason a merge is: it
377
+ # rewrites the branch a pull request is built from, and the harness
378
+ # pushes that rewrite with a lease only it can check.
379
+ UNGRANTABLE = ["git push", "gh pr merge", "git rebase"].freeze
339
380
 
340
381
  # How much of what the agent said survives into a one-line report.
341
382
  REASON_LIMIT = 300
@@ -376,6 +417,11 @@ module OpenLanes
376
417
  # and nothing here can tell the two apart.
377
418
  CREDENTIAL_VARS = %w[ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN].freeze
378
419
 
420
+ # What `claude -p --resume <id>` prints for an id it has no session for,
421
+ # in the directory it was run from (2.1.x): this line on stderr, then a
422
+ # `result` event with `is_error`, zero turns and zero tokens, exit 1.
423
+ SESSION_MISSING = /No conversation found with session ID/
424
+
379
425
  # @param env [Hash]
380
426
  # @return [Array<String>] credential variables currently set
381
427
  def self.foreign_credentials(env = ENV)
@@ -476,9 +522,15 @@ module OpenLanes
476
522
  # @param handle [OpenLanes::Execution::Executor::Handle, nil] filled in for the caller
477
523
  # @param partners [Array<OpenLanes::Agents::Agent>] the other agents working this
478
524
  # plan right now, named in the prompt beside the plan's mailbox
525
+ # @param rebase [OpenLanes::Vcs::Refresh::Outcome, nil] a rebase stopped
526
+ # on conflicts: the invocation resolves it and does no plan work
527
+ # @param resume [String, nil] the previous round's session id (M1), to
528
+ # carry on with this round's prompt; ignored by a CLI that cannot
529
+ # resume, and dropped for a fresh start when the CLI cannot find it
479
530
  # @return [OpenLanes::Execution::Executor::Result]
480
531
  # @yieldparam progress [OpenLanes::Execution::Transcript::Progress]
481
- def call(agent, subject, root: @root, round: 1, successor: nil, handle: nil, partners: [], &on_progress)
532
+ def call(agent, subject, root: @root, round: 1, successor: nil, handle: nil, partners: [], rebase: nil,
533
+ resume: nil, &on_progress)
482
534
  started = UI.monotonic
483
535
  if @dry_run
484
536
  return Result.new(ok: true,
@@ -491,12 +543,14 @@ module OpenLanes
491
543
  end
492
544
 
493
545
  handle ||= Handle.new
494
- before = head(root)
546
+ before = Boundary.snapshot(root)
495
547
  trace = trace_path(agent, subject)
548
+ handle.trace = trace
496
549
  profile = profile_for(agent, subject)
497
550
  control = Control.register(@trace_dir, "#{subject.feature.ordinal}-#{agent.name}")
498
551
  handle.control = control
499
- argv = invocation(agent, subject, root:, control:, round:, successor:, partners:)
552
+ resume = nil unless profile.adapter.resumes?
553
+ argv = invocation(agent, subject, root:, control:, round:, successor:, partners:, rebase:, resume:)
500
554
  sessions = Sessions.new
501
555
  child = launch(argv, root:, profile:, trace:, handle:, sessions:, &on_progress)
502
556
  clock = Clock.new(seconds: timeout_for(agent),
@@ -507,6 +561,15 @@ module OpenLanes
507
561
 
508
562
  begin
509
563
  status = drain(child, sessions, handle)
564
+ if resume && !handle.killed && (lost = lost_session(status, sessions.current))
565
+ UI.log("session #{resume} #{lost}, starting #{agent.name} fresh",
566
+ level: :warn,
567
+ plan: subject.feature.ordinal.to_s,
568
+ agent: agent.name)
569
+ handle.session = nil
570
+ argv = invocation(agent, subject, root:, control:, round:, successor:, partners:, rebase:)
571
+ status = drain(launch(argv, root:, profile:, trace:, handle:, sessions:, &on_progress), sessions, handle)
572
+ end
510
573
  while (compaction = handle.take_compaction)
511
574
  resumed = settling(compaction, agent) do
512
575
  carry_on(compaction, agent, subject, root:, profile:, trace:, handle:, sessions:, &on_progress)
@@ -541,7 +604,7 @@ module OpenLanes
541
604
  pid: handle.pid)
542
605
  end
543
606
 
544
- violation = boundary_violation(before, root)
607
+ violation = before.violation(root)
545
608
  return failure(sessions, started, violation, pid: handle.pid) if violation
546
609
 
547
610
  spent(sessions,
@@ -602,12 +665,15 @@ module OpenLanes
602
665
  # @param round [Integer] which attempt on this plan this is
603
666
  # @param successor [String, nil] who the agent names on its `next:` line
604
667
  # @param partners [Array<OpenLanes::Agents::Agent>]
668
+ # @param rebase [OpenLanes::Vcs::Refresh::Outcome, nil]
669
+ # @param resume [String, nil] a session id to carry on
605
670
  # @return [Array<String>]
606
- def invocation(agent, subject, root: @root, control: nil, round: 1, successor: nil, partners: [])
671
+ def invocation(agent, subject, root: @root, control: nil, round: 1, successor: nil, partners: [], rebase: nil,
672
+ resume: nil)
607
673
  profile = profile_for(agent, subject)
608
674
  profile.adapter.argv(
609
675
  Adapters::Invocation.new(
610
- prompt: prompt_for(agent, subject, root, control:, round:, successor:, partners:, profile:),
676
+ prompt: prompt_for(agent, subject, root, control:, round:, successor:, partners:, profile:, rebase:),
611
677
  root:,
612
678
  model: profile.model,
613
679
  effort: profile.effort,
@@ -615,7 +681,8 @@ module OpenLanes
615
681
  denied_tools: denied_for(agent),
616
682
  denied_commands: denied_commands(agent),
617
683
  network: agent.network,
618
- lean: @lean
684
+ lean: @lean,
685
+ resume:
619
686
  )
620
687
  )
621
688
  end
@@ -659,6 +726,33 @@ module OpenLanes
659
726
  child
660
727
  end
661
728
 
729
+ # Whether a resumed invocation failed because its session is gone,
730
+ # rather than at anything it did, and what to say about it. The message
731
+ # is the sure sign; a failure that spent nothing and called no tool is
732
+ # taken as the same thing, because a CLI that rewords the message must
733
+ # still not cost the round. Starting such a failure again fresh costs
734
+ # one more start-up at worst: an invocation that spent nothing has
735
+ # nothing to lose.
736
+ #
737
+ # That second case also catches an overload, an auth error or a network
738
+ # drop at start-up, which the fresh start will usually hit again. The
739
+ # line the CLI printed goes into the log beside the retry, so an outage
740
+ # reads as an outage rather than as a lost session.
741
+ #
742
+ # @param status [Process::Status]
743
+ # @param transcript [OpenLanes::Execution::Transcript]
744
+ # @return [String, nil] why the session counts as lost, nil when it does not
745
+ def lost_session(status, transcript)
746
+ return nil if status.success? && !transcript.failed?
747
+
748
+ said = [transcript.error, *transcript.plain].map(&:to_s).reject(&:empty?)
749
+ return "not found" if said.any? { SESSION_MISSING.match?(it) }
750
+ return nil unless transcript.up.zero? && transcript.down.zero? && transcript.tools.zero?
751
+
752
+ detail = said.empty? ? "exit #{status.exitstatus}" : said.last.lines.first.strip[0, 200]
753
+ "failed before spending anything (#{detail})"
754
+ end
755
+
662
756
  # @return [Process::Status]
663
757
  def drain(child, sessions, handle)
664
758
  child.each_chunk do |out|
@@ -794,10 +888,12 @@ module OpenLanes
794
888
  # @param round [Integer]
795
889
  # @param successor [String, nil]
796
890
  # @param partners [Array<OpenLanes::Agents::Agent>]
891
+ # @param rebase [OpenLanes::Vcs::Refresh::Outcome, nil]
797
892
  # @return [String]
798
893
  def prompt_for(agent, subject, root = @root, control: nil, round: 1, successor: nil, partners: [],
799
- profile: profile_for(agent, subject))
894
+ profile: profile_for(agent, subject), rebase: nil)
800
895
  Prompt.new(agent:,
896
+ rebase:,
801
897
  subject:,
802
898
  root:,
803
899
  profile:,
@@ -827,23 +923,6 @@ module OpenLanes
827
923
  handle.kill!(grace: 0, reason: :quit)
828
924
  end
829
925
  end
830
-
831
- # @return [String, nil] current commit, or nil outside a repository
832
- def head(root = @root)
833
- out = `git -C #{root.shellescape} rev-parse HEAD 2>/dev/null`.strip
834
- out.empty? ? nil : out
835
- end
836
-
837
- # @param before [String, nil]
838
- # @return [String, nil] what boundary was crossed, or nil
839
- def boundary_violation(before, root = @root)
840
- return nil if before.nil?
841
-
842
- after = head(root)
843
- return "agent committed (HEAD moved #{before[0, 7]} → #{after[0, 7]}) — the boundary is docs plus code, no commits" if after != before
844
-
845
- nil
846
- end
847
926
  end
848
927
  end
849
928
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "etc"
4
+ require "open3"
5
+ require "time"
6
+
7
+ module OpenLanes
8
+ module Execution
9
+ # When a process started, so a recovering engine can tell its own agent
10
+ # from a stranger that inherited the pid. Ruby's stdlib has no call for
11
+ # it: Linux answers through `/proc/<pid>/stat`, macOS through
12
+ # `ps -o lstart=`. The engine runs this, never an agent, so the autonomy
13
+ # boundary's withheld commands do not apply.
14
+ module ProcessStart
15
+ module_function
16
+
17
+ # @param pid [Integer]
18
+ # @param proc_root [String] where `/proc` is, a fixture in specs
19
+ # @param runner [#capture2] runs `ps`, injected in specs
20
+ # @return [Time, nil] the start time, or nil where it cannot be read
21
+ def of(pid, proc_root: "/proc", runner: Open3)
22
+ return nil unless pid.is_a?(Integer) && pid.positive?
23
+
24
+ from_proc(pid, proc_root) || from_ps(pid, runner)
25
+ end
26
+
27
+ # Field 22 of `stat` is the start in clock ticks after boot. The command
28
+ # name (field 2) may hold spaces and parentheses, so fields are counted
29
+ # from the last `)`.
30
+ #
31
+ # @api private
32
+ def from_proc(pid, proc_root)
33
+ stat = File.join(proc_root, pid.to_s, "stat")
34
+ uptime = File.join(proc_root, "uptime")
35
+ return nil unless File.file?(stat) && File.file?(uptime)
36
+
37
+ ticks = File.read(stat).split(")").last.split[19].to_i
38
+ booted = Time.now - File.read(uptime).split.first.to_f
39
+ booted + (ticks / clock_ticks.to_f)
40
+ rescue SystemCallError, NoMethodError
41
+ nil
42
+ end
43
+
44
+ # @api private
45
+ def from_ps(pid, runner)
46
+ out, status = runner.capture2("ps", "-o", "lstart=", "-p", pid.to_s)
47
+ return nil unless status.success?
48
+
49
+ text = out.strip
50
+ text.empty? ? nil : Time.parse(text)
51
+ rescue SystemCallError, ArgumentError
52
+ nil
53
+ end
54
+
55
+ # @api private
56
+ def clock_ticks
57
+ Etc.sysconf(Etc::SC_CLK_TCK)
58
+ rescue StandardError
59
+ 100
60
+ end
61
+ end
62
+ end
63
+ end