openlanes 3.0.1 → 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 (319) 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 +4 -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 +18 -4
  65. data/lib/openlanes/adapters/capabilities.rb +29 -0
  66. data/lib/openlanes/adapters/claude.rb +14 -11
  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 +7 -0
  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 -486
  106. data/lib/openlanes/cli/setup/setup.rb +51 -0
  107. data/lib/openlanes/cli/start/start.rb +7 -51
  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 +84 -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 +1175 -117
  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 +6 -6
  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 +12 -120
  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 +223 -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 +17 -13
  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 +60 -11
  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 +217 -30
  182. data/lib/openlanes/live/copilot/slash.rb +82 -0
  183. data/lib/openlanes/live/copilot.rb +38 -4
  184. data/lib/openlanes/live/event_bus/base.rb +52 -0
  185. data/lib/openlanes/live/event_bus/memory_bus.rb +14 -0
  186. data/lib/openlanes/live/event_bus/redis_bus.rb +10 -0
  187. data/lib/openlanes/live/event_bus/transaction.rb +27 -0
  188. data/lib/openlanes/live/event_bus.rb +29 -0
  189. data/lib/openlanes/live/interviewer.rb +146 -0
  190. data/lib/openlanes/live/keys.rb +56 -0
  191. data/lib/openlanes/live/mail.rb +143 -0
  192. data/lib/openlanes/live/memory_store.rb +12 -1
  193. data/lib/openlanes/live/news.rb +61 -0
  194. data/lib/openlanes/live/node.rb +20 -0
  195. data/lib/openlanes/live/outage.rb +225 -0
  196. data/lib/openlanes/live/outage_report.rb +89 -0
  197. data/lib/openlanes/live/publisher.rb +42 -2
  198. data/lib/openlanes/live/run_record.rb +39 -1
  199. data/lib/openlanes/live/run_state.rb +19 -2
  200. data/lib/openlanes/live/session.rb +31 -3
  201. data/lib/openlanes/live/snapshot.rb +103 -42
  202. data/lib/openlanes/live/stations.rb +4 -4
  203. data/lib/openlanes/live/store.rb +18 -1
  204. data/lib/openlanes/live/subscriber.rb +22 -2
  205. data/lib/openlanes/live.rb +66 -1
  206. data/lib/openlanes/plans/blocked_file.rb +117 -0
  207. data/lib/openlanes/plans/choices.rb +228 -0
  208. data/lib/openlanes/plans/dependencies.rb +304 -0
  209. data/lib/openlanes/plans/feature.rb +75 -16
  210. data/lib/openlanes/plans/journal.rb +169 -0
  211. data/lib/openlanes/plans/last_known_state.rb +82 -0
  212. data/lib/openlanes/plans/lock.rb +171 -0
  213. data/lib/openlanes/plans/mailbox.rb +145 -4
  214. data/lib/openlanes/plans/plan_state.rb +590 -9
  215. data/lib/openlanes/plans/pull_request_machine.rb +163 -0
  216. data/lib/openlanes/plans/review_label.rb +139 -0
  217. data/lib/openlanes/plans/spec.rb +239 -3
  218. data/lib/openlanes/plans/state_machine.rb +63 -30
  219. data/lib/openlanes/plans/status.rb +60 -11
  220. data/lib/openlanes/plans/subject.rb +4 -0
  221. data/lib/openlanes/plans/tree.rb +101 -10
  222. data/lib/openlanes/plans/verdict.rb +124 -0
  223. data/lib/openlanes/plans.rb +19 -0
  224. data/lib/openlanes/presentation/api_docs.rb +76 -0
  225. data/lib/openlanes/presentation/documentation.rb +105 -12
  226. data/lib/openlanes/presentation/pr_changes.rb +90 -0
  227. data/lib/openlanes/presentation/reporter.rb +64 -5
  228. data/lib/openlanes/presentation.rb +20 -1
  229. data/lib/openlanes/setup/check.rb +16 -0
  230. data/lib/openlanes/setup/machine.rb +179 -0
  231. data/lib/openlanes/setup.rb +34 -0
  232. data/lib/openlanes/support/clock.rb +57 -0
  233. data/lib/openlanes/support/config.rb +318 -6
  234. data/lib/openlanes/support/log.rb +206 -0
  235. data/lib/openlanes/support/refusal.rb +15 -0
  236. data/lib/openlanes/support/runtime.rb +61 -0
  237. data/lib/openlanes/support/spawner.rb +51 -0
  238. data/lib/openlanes/support/ui.rb +39 -24
  239. data/lib/openlanes/vcs/ci_gate.rb +31 -0
  240. data/lib/openlanes/vcs/github.rb +362 -12
  241. data/lib/openlanes/vcs/label_migration.rb +132 -0
  242. data/lib/openlanes/vcs/label_sync.rb +75 -0
  243. data/lib/openlanes/vcs/publisher.rb +226 -8
  244. data/lib/openlanes/vcs/refresh.rb +337 -0
  245. data/lib/openlanes/vcs/rename_date.rb +83 -0
  246. data/lib/openlanes/vcs/review.rb +66 -0
  247. data/lib/openlanes/vcs/worktree.rb +167 -6
  248. data/lib/openlanes/vcs.rb +18 -0
  249. data/lib/openlanes/version.rb +1 -1
  250. data/lib/openlanes/web/admin_auth.rb +104 -0
  251. data/lib/openlanes/web/app.rb +115 -16
  252. data/lib/openlanes/web/context.rb +15 -9
  253. data/lib/openlanes/web/demo.rb +29 -14
  254. data/lib/openlanes/web/demo_board.rb +63 -5
  255. data/lib/openlanes/web/events.rb +86 -7
  256. data/lib/openlanes/web/launcher.rb +69 -7
  257. data/lib/openlanes/web/listen.rb +133 -0
  258. data/lib/openlanes/web/plan_files.rb +136 -0
  259. data/lib/openlanes/web/request_log.rb +108 -0
  260. data/lib/openlanes/web/server.rb +29 -10
  261. data/lib/openlanes/web.rb +6 -4
  262. data/lib/openlanes.rb +45 -7
  263. data/openlanes.gemspec +9 -4
  264. data/schemas/journal-event.schema.json +72 -0
  265. data/schemas/plan-state.schema.json +163 -2
  266. data/skills/openlanes/SKILL.md +2 -2
  267. data/skills/openlanes-documents/SKILL.md +4 -4
  268. data/skills/openlanes-documents/references/blocked-template.md +2 -2
  269. data/skills/openlanes-documents/references/closing-templates.md +1 -1
  270. data/skills/openlanes-documents/references/plan-template.md +12 -4
  271. data/skills/openlanes-documents/references/retroactive-spec.md +1 -1
  272. data/skills/openlanes-run/SKILL.md +5 -5
  273. data/skills/openlanes-spec/SKILL.md +1 -1
  274. data/skills/openlanes-status/SKILL.md +1 -1
  275. data/web/dist/assets/{index-BFPI-zXI.js → index-690ayLS4.js} +2 -2
  276. data/web/dist/assets/index-6ZmW4slI.js +199 -0
  277. data/web/dist/assets/{index-bpsFIpXE.js → index-B5KzlXp1.js} +2 -2
  278. data/web/dist/assets/{index-DtA46i32.js → index-BAo_v9bU.js} +2 -2
  279. data/web/dist/assets/{index-CnJfB0l2.js → index-BDm7g-xG.js} +2 -2
  280. data/web/dist/assets/{index-DXgeajd6.js → index-BFH9bC7v.js} +2 -2
  281. data/web/dist/assets/{index-DH0gDlee.js → index-BVHPV7z4.js} +2 -2
  282. data/web/dist/assets/{index-DaSjQ-H4.js → index-B_CPqjnA.js} +2 -2
  283. data/web/dist/assets/{index-Dbxvk-EC.js → index-BhwBH5ct.js} +2 -2
  284. data/web/dist/assets/{index-6MZkmh3k.js → index-Bq5AdwEt.js} +2 -2
  285. data/web/dist/assets/{index-B5CKJPm-.js → index-CAW6ClM2.js} +2 -2
  286. data/web/dist/assets/index-Cp86u27u.css +1 -0
  287. data/web/dist/assets/{index-CnvkZu5Y.js → index-Culm4pMO.js} +2 -2
  288. data/web/dist/assets/{index-IqD8mBos.js → index-DElDJfp1.js} +2 -2
  289. data/web/dist/assets/{index-C3zc4gFe.js → index-Dja7osNW.js} +2 -2
  290. data/web/dist/assets/{index-C2jwoWVV.js → index-DjhU3FzU.js} +2 -2
  291. data/web/dist/assets/{index-pvJi-JJO.js → index-Dutbxj4o.js} +2 -2
  292. data/web/dist/assets/{index-BF6QYroq.js → index-RkDhPMDt.js} +2 -2
  293. data/web/dist/assets/{index-DbueqOK5.js → index-_mptZjlC.js} +2 -2
  294. data/web/dist/index.html +2 -2
  295. data/web/src/api/fixtures/board.json +203 -28
  296. metadata +1053 -889
  297. data/bin/factory-e2e +0 -157
  298. data/bin/plan-number +0 -164
  299. data/bin/setup +0 -26
  300. data/evals/cases/palpatine-planner/plan-deep/fail/plan/plan.md +0 -8
  301. data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan.md +0 -12
  302. data/web/dist/assets/index-BYJCWlk0.js +0 -198
  303. data/web/dist/assets/index-CjInBi5X.css +0 -1
  304. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/plan/plan-backend.md +0 -0
  305. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/plan/spec.md +0 -0
  306. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/run.json +0 -0
  307. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan-backend.md +0 -0
  308. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan-frontend.md +0 -0
  309. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/spec.md +0 -0
  310. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/run.json +0 -0
  311. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/plan-backend.md +0 -0
  312. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/plan-frontend.md +0 -0
  313. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/spec.md +0 -0
  314. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/run.json +0 -0
  315. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan-backend.md +0 -0
  316. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan-frontend.md +0 -0
  317. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/spec.md +0 -0
  318. /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/run.json +0 -0
  319. /data/exe/{depot → ol} +0 -0
data/lib/openlanes.rb CHANGED
@@ -6,7 +6,10 @@ require "dry/cli"
6
6
  require "dry/cli/help"
7
7
 
8
8
  require "dry/inflector"
9
+ # Every command workflow answers a Dry::Monads::Result (see OpenLanes::Refusal).
10
+ require "dry/monads"
9
11
  require "fileutils"
12
+ require "open3"
10
13
  require "shellwords"
11
14
  require "zeitwerk"
12
15
  loader = Zeitwerk::Loader.for_gem
@@ -21,13 +24,16 @@ loader.inflector.inflect(
21
24
  "ui" => "UI"
22
25
  )
23
26
  loader.collapse("#{__dir__}/openlanes/support")
27
+ loader.collapse("#{__dir__}/openlanes/cli/coverage")
24
28
  loader.collapse("#{__dir__}/openlanes/cli/create")
25
29
  loader.collapse("#{__dir__}/openlanes/cli/docs")
26
30
  loader.collapse("#{__dir__}/openlanes/cli/eval")
27
31
  loader.collapse("#{__dir__}/openlanes/cli/index")
28
32
  loader.collapse("#{__dir__}/openlanes/cli/init")
29
33
  loader.collapse("#{__dir__}/openlanes/cli/list_plans")
34
+ loader.collapse("#{__dir__}/openlanes/cli/review")
30
35
  loader.collapse("#{__dir__}/openlanes/cli/run")
36
+ loader.collapse("#{__dir__}/openlanes/cli/setup")
31
37
  loader.collapse("#{__dir__}/openlanes/cli/start")
32
38
  loader.collapse("#{__dir__}/openlanes/cli/states")
33
39
  loader.collapse("#{__dir__}/openlanes/cli/unblock")
@@ -40,9 +46,13 @@ loader.collapse("#{__dir__}/openlanes/cli/mail/subcommands")
40
46
  loader.collapse("#{__dir__}/openlanes/cli/state/subcommands")
41
47
  loader.collapse("#{__dir__}/openlanes/cli/resync/subcommands")
42
48
  loader.collapse("#{__dir__}/openlanes/cli/skills/subcommands")
49
+ loader.collapse("#{__dir__}/openlanes/cli/api/subcommands")
50
+ loader.ignore("#{__dir__}/openlanes/cli/api/api.rb")
51
+ loader.collapse("#{__dir__}/openlanes/cli/app/subcommands")
43
52
  loader.ignore("#{__dir__}/openlanes/cli/linear/linear.rb")
44
53
  loader.ignore("#{__dir__}/openlanes/cli/mail/mail.rb")
45
54
  loader.ignore("#{__dir__}/openlanes/cli/state/state.rb")
55
+ loader.ignore("#{__dir__}/openlanes/cli/app/app.rb")
46
56
  loader.ignore("#{__dir__}/openlanes/linear/mapping.rb")
47
57
  loader.setup
48
58
 
@@ -51,8 +61,8 @@ loader.setup
51
61
  # Three phases, each with a file that proves it happened, and a guarded state
52
62
  # machine that refuses to let a plan folder claim a phase it has not reached.
53
63
  #
54
- # spec ⚪️ New spec.md
55
- # plan ⭐️ Ready plan.md
64
+ # spec ⚪ New spec.md
65
+ # plan ⭐ Ready plan.md
56
66
  # build 🟡 → ✅ pull-requests.md
57
67
  #
58
68
  # The conventions are not documented anywhere else by hand: the status
@@ -95,6 +105,8 @@ module OpenLanes
95
105
  def self.move_directory(source, target)
96
106
  return false if File.exist?(target)
97
107
 
108
+ # Into archive/ and friends, which the first move there creates.
109
+ FileUtils.mkdir_p(File.dirname(target))
98
110
  parent = File.dirname(source)
99
111
  tracked = system("git",
100
112
  "-C",
@@ -116,7 +128,8 @@ module OpenLanes
116
128
  true
117
129
  end
118
130
 
119
- # Plan numbers visible in `.plans` on a git ref, without checking it out.
131
+ # Plan numbers visible in `.plans` on a git ref, without checking it out,
132
+ # the inactive subfolders included: an archived plan keeps its number.
120
133
  #
121
134
  # A pull request's branch is the only place that knows how far the sequence
122
135
  # had got when the work started, and branches are not all rebased onto the
@@ -127,14 +140,39 @@ module OpenLanes
127
140
  # @return [Array<OpenLanes::Plans::Ordinal>] possibly empty
128
141
  def self.plans_on_ref(root, ref)
129
142
  ["origin/#{ref}", ref].each do |candidate|
130
- out = `git -C #{root.shellescape} ls-tree -d --name-only #{candidate.shellescape} #{PLANS_DIR}/ 2>/dev/null`
131
- next if out.to_s.strip.empty?
132
-
133
- return out.lines.filter_map { |line| Plans::Ordinal.from_dirname(File.basename(line.strip)) }
143
+ found = plans_at(root, candidate)
144
+ return found unless found.empty?
134
145
  end
135
146
  []
136
147
  end
137
148
 
149
+ # Plan numbers in `.plans` on one tree-ish, the inactive subfolders
150
+ # included. {plans_on_ref} guesses at a branch name; this reads exactly
151
+ # what it is given, which is what allocating a number across every branch
152
+ # needs: a full ref name tried as `origin/refs/heads/x` would cost a
153
+ # second `ls-tree` per branch for nothing.
154
+ #
155
+ # @param root [String] any directory inside the repository
156
+ # @param treeish [String] a ref, or a commit
157
+ # @param plans [String] the `.plans` directory, relative to +root+
158
+ # @return [Array<OpenLanes::Plans::Ordinal>] empty when the ref is unknown
159
+ # or carries no plans
160
+ def self.plans_at(root, treeish, plans: PLANS_DIR)
161
+ dirs = [plans, *Plans::Tree::SUBFOLDERS.map { "#{plans}/#{it}" }].map { "#{it}/" }
162
+ out, status = Open3.capture2("git",
163
+ "-C",
164
+ root,
165
+ "ls-tree",
166
+ "-d",
167
+ "--name-only",
168
+ treeish,
169
+ *dirs,
170
+ err: File::NULL)
171
+ return [] unless status.success?
172
+
173
+ out.lines.filter_map { |line| Plans::Ordinal.from_dirname(File.basename(line.strip)) }
174
+ end
175
+
138
176
  # The agent that writes a specification for work that already shipped.
139
177
  # Named here rather than in the CLI so the definition file stays the single
140
178
  # source of truth about who does what.
data/openlanes.gemspec CHANGED
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
17
17
  TEXT
18
18
  spec.homepage = "https://github.com/kigster/openlanes"
19
19
 
20
- spec.required_ruby_version = ">= 3" # rubocop:disable Gemspec/RequiredRubyVersion -- the floor users get, not the Ruby the cops lint for
20
+ spec.required_ruby_version = ">= 4" # rubocop:disable Gemspec/RequiredRubyVersion -- the floor users get, not the Ruby the cops lint for
21
21
 
22
22
  spec.metadata = {
23
23
  "homepage_uri" => spec.homepage,
@@ -33,7 +33,7 @@ Gem::Specification.new do |spec|
33
33
  # `openlanes web --demo` replays.
34
34
  # Source maps stay out: they double the page's size for nobody who installs it.
35
35
  globbed = Dir.glob(["*.gemspec", "lib/**/*.rb", "agents/*.md", "schemas/*.json", "evals/cases/**/*", "skills/**/*",
36
- "exe/*", "bin/*", "web/dist/**/*", "web/src/api/fixtures/board.json"])
36
+ "exe/*", "web/dist/**/*", "web/src/api/fixtures/board.json", "Brewfile"])
37
37
  globbed.select { |f| File.file?(f) && !f.end_with?(".map") }
38
38
  end
39
39
 
@@ -49,10 +49,10 @@ Gem::Specification.new do |spec|
49
49
  end
50
50
 
51
51
  spec.bindir = "exe"
52
- # `depot` and `lanes` are symlinks to `openlanes` in the checkout; named here
52
+ # `ol` and `lanes` are symlinks to `openlanes` in the checkout; named here
53
53
  # so that installing the gem writes all three, rather than only the one git
54
54
  # recorded.
55
- spec.executables = %w[openlanes depot lanes]
55
+ spec.executables = %w[openlanes ol lanes]
56
56
  spec.require_paths = ["lib"]
57
57
 
58
58
  spec.add_dependency "aasm"
@@ -66,6 +66,7 @@ Gem::Specification.new do |spec|
66
66
  spec.add_dependency "dry-inflector"
67
67
  spec.add_dependency "dry-monads"
68
68
  spec.add_dependency "fuzzy-string-match"
69
+ spec.add_dependency "inquirex-llm", "~> 0.12"
69
70
  # Opens the control room in the browser when `openlanes run` starts the API server.
70
71
  spec.add_dependency "launchy", "~> 3.1"
71
72
  spec.add_dependency "parallel"
@@ -76,7 +77,9 @@ Gem::Specification.new do |spec|
76
77
  spec.add_dependency "rackup", "~> 2.1"
77
78
  # The live run (plan 004) lives in Redis: board and command streams, plan claims.
78
79
  spec.add_dependency "redis-client", "~> 0.31"
80
+ spec.add_dependency "semantic_logger", "~> 5.1"
79
81
  spec.add_dependency "sinatra", "~> 4.2"
82
+ spec.add_dependency "agent-lock"
80
83
  spec.add_dependency "tty-box"
81
84
  spec.add_dependency "tty-command"
82
85
  spec.add_dependency "tty-cursor"
@@ -84,4 +87,6 @@ Gem::Specification.new do |spec|
84
87
  spec.add_dependency "tty-screen"
85
88
  spec.add_dependency "unicode-display_width"
86
89
  spec.add_dependency "zeitwerk"
90
+ spec.add_dependency "newrelic_rpm"
91
+ spec.add_dependency "newrelic-infinite_tracing"
87
92
  end
@@ -0,0 +1,72 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/kigster/openlanes/schemas/journal-event.schema.json",
4
+ "title": "openlanes engine journal event",
5
+ "description": "One line of a node's NDJSON progress journal, written by OpenLanes::Engine::EventJournal under Runtime.journal_dir. Append-only, fsync'd per event.",
6
+ "type": "object",
7
+ "required": ["v", "seq", "at", "node", "run_id", "kind"],
8
+ "properties": {
9
+ "v": { "const": 1 },
10
+ "seq": { "type": "integer", "minimum": 1 },
11
+ "at": { "type": "string", "format": "date-time" },
12
+ "node": { "type": "string", "minLength": 1 },
13
+ "run_id": { "type": "string", "minLength": 1 },
14
+ "kind": {
15
+ "enum": [
16
+ "run_started", "run_ended", "plan_assigned", "agent_started", "agent_session",
17
+ "agent_offset", "agent_ended", "transition", "extension", "question"
18
+ ]
19
+ },
20
+ "plan": { "type": "string", "pattern": "^\\d{3}\\.\\d{2}$" },
21
+ "agent": { "type": "string", "minLength": 1 },
22
+ "round": { "type": "integer", "minimum": 0 },
23
+ "pid": { "type": "integer", "minimum": 1 },
24
+ "started_at": { "type": ["string", "null"], "format": "date-time" },
25
+ "session_id": { "type": ["string", "null"] },
26
+ "output_file": { "type": ["string", "null"] },
27
+ "offset": { "type": "integer", "minimum": 0 },
28
+ "exit": { "type": ["integer", "null"] },
29
+ "from": { "type": "string" },
30
+ "to": { "type": "string" },
31
+ "event": { "type": "string" },
32
+ "seconds": { "type": "number", "minimum": 0 },
33
+ "note": { "type": "string" },
34
+ "reason": { "type": "string" },
35
+ "jobs": { "type": "integer", "minimum": 1 },
36
+ "isolation": { "type": "string" }
37
+ },
38
+ "allOf": [
39
+ {
40
+ "if": { "properties": { "kind": { "const": "plan_assigned" } } },
41
+ "then": { "required": ["plan", "agent"] }
42
+ },
43
+ {
44
+ "if": { "properties": { "kind": { "const": "agent_started" } } },
45
+ "then": { "required": ["plan", "agent", "pid", "started_at", "session_id", "output_file"] }
46
+ },
47
+ {
48
+ "if": { "properties": { "kind": { "const": "agent_session" } } },
49
+ "then": { "required": ["plan", "agent", "session_id"], "properties": { "session_id": { "type": "string" } } }
50
+ },
51
+ {
52
+ "if": { "properties": { "kind": { "const": "agent_offset" } } },
53
+ "then": { "required": ["plan", "agent", "offset"] }
54
+ },
55
+ {
56
+ "if": { "properties": { "kind": { "const": "agent_ended" } } },
57
+ "then": { "required": ["plan", "agent", "exit"] }
58
+ },
59
+ {
60
+ "if": { "properties": { "kind": { "const": "transition" } } },
61
+ "then": { "required": ["plan", "from", "to"] }
62
+ },
63
+ {
64
+ "if": { "properties": { "kind": { "const": "extension" } } },
65
+ "then": { "required": ["plan", "agent", "seconds"] }
66
+ },
67
+ {
68
+ "if": { "properties": { "kind": { "const": "question" } } },
69
+ "then": { "required": ["plan", "agent", "note"] }
70
+ }
71
+ ]
72
+ }
@@ -22,9 +22,41 @@
22
22
  "description": "The last state the harness moved the folder to. The folder name is authoritative.",
23
23
  "type": ["string", "null"]
24
24
  },
25
+ "moves": {
26
+ "description": "How many times openlanes has moved the folder to another state. A settling stage notes the count, so a later move, even away and back, is seen.",
27
+ "type": "integer",
28
+ "minimum": 0
29
+ },
25
30
  "updated_at": { "type": ["string", "null"], "format": "date-time" },
31
+ "quality_ask": {
32
+ "description": "R4's question, desired_codebase_quality, asked once per project on the first review-ok-not-perfect pull request while .openlanes.yml has no key; settled when a person answers.",
33
+ "type": "object",
34
+ "required": ["asked_at"],
35
+ "additionalProperties": false,
36
+ "properties": {
37
+ "asked_at": { "type": "string", "format": "date-time" },
38
+ "answer": { "type": "string", "enum": ["high", "mvp"] },
39
+ "by": { "type": "string", "minLength": 1 },
40
+ "settled_at": { "type": "string", "format": "date-time" }
41
+ }
42
+ },
43
+ "resume_until": {
44
+ "description": "Until when a 💣 on this plan does not halt the run: the control room's Resume anyway for 24 h.",
45
+ "type": ["string", "null"],
46
+ "format": "date-time"
47
+ },
48
+ "finished": {
49
+ "description": "When and how the plan finished: hansolo approved it, or its pull request merged. A merge replaces an approval. Cleared when an agent starts on the plan again, or a person rejects it.",
50
+ "type": "object",
51
+ "required": ["kind", "at"],
52
+ "additionalProperties": false,
53
+ "properties": {
54
+ "kind": { "enum": ["approved", "merged"] },
55
+ "at": { "type": "string", "format": "date-time" }
56
+ }
57
+ },
26
58
  "stages": {
27
- "description": "One record per agent per round, written by the harness.",
59
+ "description": "One record per dispatch of an agent's round, in dispatch order, written by the harness.",
28
60
  "type": "array",
29
61
  "items": {
30
62
  "type": "object",
@@ -36,6 +68,7 @@
36
68
  "status": { "$ref": "#/$defs/status" },
37
69
  "from": { "type": "string" },
38
70
  "to": { "type": "string" },
71
+ "moves": { "description": "The plan's moves when this round settled.", "type": "integer", "minimum": 0 },
39
72
  "started_at": { "type": "string", "format": "date-time" },
40
73
  "finished_at": { "type": "string", "format": "date-time" },
41
74
  "adapter": { "enum": ["claude", "codex", "pi"] },
@@ -45,8 +78,17 @@
45
78
  "down": { "type": "integer", "minimum": 0 },
46
79
  "seconds": { "type": "number", "minimum": 0 },
47
80
  "note": { "type": "string" },
81
+ "session": {
82
+ "description": "The CLI's session id for this round, as soon as its stream named it. `claude --resume <id>`, run from `cwd`, carries it on by hand.",
83
+ "type": "string"
84
+ },
85
+ "cwd": {
86
+ "description": "The checkout the round ran in. A claude session is found only from the directory it ran in.",
87
+ "type": "string"
88
+ },
48
89
  "run": {
49
- "description": "The harness process that dispatched it, so a restart can tell a crash from a live run.",
90
+ "description": "Deprecated: written by releases before 021.00, read so their crashes still resume. The harness pid and host now live in the run's state file outside the repository.",
91
+ "deprecated": true,
50
92
  "type": "object",
51
93
  "required": ["pid", "host"],
52
94
  "additionalProperties": false,
@@ -75,6 +117,84 @@
75
117
  }
76
118
  }
77
119
  },
120
+ "last-known-state": {
121
+ "description": "One resume record per agent, last one wins: written by the agent through `openlanes state resume`, or by the harness when the agent could not.",
122
+ "type": "object",
123
+ "propertyNames": { "$ref": "#/$defs/agent" },
124
+ "additionalProperties": { "$ref": "#/$defs/lastKnownState" }
125
+ },
126
+ "merge_risk": {
127
+ "description": "palpatine-risk-assessor's verdicts for this visit to Completed (P1, P4), by pull request number, and when the engine rebased a sibling onto main (P5). Stands only while moves still equals the count it was written at.",
128
+ "type": "object",
129
+ "required": ["moves", "at", "verdicts", "rebased"],
130
+ "additionalProperties": false,
131
+ "properties": {
132
+ "moves": { "type": "integer", "minimum": 0 },
133
+ "at": { "type": "string", "format": "date-time" },
134
+ "verdicts": { "type": "object", "additionalProperties": { "enum": ["auto", "manual"] } },
135
+ "rebased": { "type": "object", "additionalProperties": { "type": "string", "format": "date-time" } }
136
+ }
137
+ },
138
+ "memory": {
139
+ "description": "Each agent's journal (M2): what it printed as one-line JSON while it worked, kept by the harness, the only writer. Entries are numbered 1 up across an agent's rounds, in order; a later one wins.",
140
+ "type": "object",
141
+ "propertyNames": { "$ref": "#/$defs/agent" },
142
+ "additionalProperties": {
143
+ "type": "object",
144
+ "required": ["rounds"],
145
+ "additionalProperties": false,
146
+ "properties": {
147
+ "rounds": {
148
+ "description": "Keyed by the agent's journal round on this plan, 1 up across runs and states; not the dispatcher's round, which restarts in every run.",
149
+ "type": "object",
150
+ "propertyNames": { "pattern": "^[1-9][0-9]*$" },
151
+ "additionalProperties": { "$ref": "#/$defs/journalRound" }
152
+ }
153
+ }
154
+ }
155
+ },
156
+ "conversions": {
157
+ "description": "Questions in blocked.md the engine rewrote into lettered choices (D9), and ones it tried and could not, with the text as the agent wrote it.",
158
+ "type": "array",
159
+ "items": {
160
+ "type": "object",
161
+ "required": ["question", "digest", "original", "at", "model", "outcome"],
162
+ "additionalProperties": false,
163
+ "properties": {
164
+ "question": { "type": "integer", "minimum": 1, "description": "The n of ## Bn." },
165
+ "digest": { "type": "string", "pattern": "^[0-9a-f]{64}$", "description": "SHA256 of original, so the same text is never sent twice." },
166
+ "original": { "type": "string", "minLength": 1 },
167
+ "at": { "type": "string", "format": "date-time" },
168
+ "model": { "type": "string", "minLength": 1 },
169
+ "outcome": { "enum": ["converted", "rejected"] },
170
+ "note": { "type": "string", "description": "Why the model's answer was rejected." }
171
+ }
172
+ }
173
+ },
174
+ "extensions": {
175
+ "description": "The harness asking a person for more time or another round for one agent (E1), and how each request was settled.",
176
+ "type": "array",
177
+ "items": {
178
+ "type": "object",
179
+ "required": ["number", "agent", "state", "round", "limit", "asked_at"],
180
+ "additionalProperties": false,
181
+ "properties": {
182
+ "number": { "type": "integer", "minimum": 1, "description": "The n of Xn, unique in this plan." },
183
+ "agent": { "type": "string", "minLength": 1 },
184
+ "state": { "type": "string", "minLength": 1, "description": "The status key the agent was working when it ran out." },
185
+ "round": { "type": "integer", "minimum": 1 },
186
+ "limit": { "enum": ["time", "rounds"], "description": "What ran out: the round's clock, or the agent's rounds." },
187
+ "asked_at": { "type": "string", "format": "date-time" },
188
+ "answer": { "enum": ["a", "b", "c"], "description": "a) one more round, b) ten more minutes, c) twenty more minutes." },
189
+ "by": { "type": "string", "minLength": 1, "description": "Who answered: a person's user name, or auto." },
190
+ "grant": { "enum": ["live", "round"], "description": "live: the running clock was extended; round: one more round." },
191
+ "seconds": { "type": "integer", "minimum": 0, "description": "Extra seconds on the clock the grant gives." },
192
+ "closed": { "type": "string", "minLength": 1, "description": "Why it was settled without an answer." },
193
+ "settled_at": { "type": "string", "format": "date-time" },
194
+ "spent": { "type": "boolean", "description": "A round grant's seconds were given to the round that ran on them." }
195
+ }
196
+ }
197
+ },
78
198
  "messages": {
79
199
  "description": "What agents on this plan said to each other, through `openlanes mail send`.",
80
200
  "type": "array",
@@ -83,17 +203,58 @@
83
203
  "required": ["number", "at", "from", "to", "body"],
84
204
  "additionalProperties": false,
85
205
  "properties": {
206
+ "id": { "type": "string", "minLength": 1, "description": "Written by `openlanes mail send`, so a message drained from Redis is recognised by identity, not by a number another copy of this file may also have given out. Absent on messages written before ids existed." },
86
207
  "number": { "type": "integer", "minimum": 1 },
87
208
  "at": { "type": "string", "format": "date-time" },
88
209
  "from": { "$ref": "#/$defs/agent" },
89
210
  "to": { "$ref": "#/$defs/agent" },
90
211
  "body": { "type": "string", "minLength": 1 },
212
+ "read": { "enum": ["yes", "no"], "description": "\"yes\" once its recipient ran `openlanes mail ack`. Absent on messages written before acks existed, which read as \"no\"." },
91
213
  "command": { "type": "string", "minLength": 1, "description": "The id of the page's command that sent it (plan 004, W13), so a redelivered command appends nothing twice." }
92
214
  }
93
215
  }
94
216
  }
95
217
  },
96
218
  "$defs": {
219
+ "lastKnownState": {
220
+ "description": "Where one agent stood when it was interrupted. No pid, token or exit field exists: those describe one machine's run and stay in openlanes-state.json.",
221
+ "type": "object",
222
+ "required": ["source", "at", "round", "status"],
223
+ "additionalProperties": false,
224
+ "properties": {
225
+ "source": { "enum": ["agent", "harness"] },
226
+ "at": { "type": "string", "format": "date-time" },
227
+ "round": { "type": "integer", "minimum": 1 },
228
+ "status": { "$ref": "#/$defs/status" },
229
+ "done": { "type": "array", "items": { "type": "string" } },
230
+ "remaining": { "type": "array", "items": { "type": "string" } },
231
+ "next_step": { "type": "string" },
232
+ "files_touched": { "type": "array", "items": { "type": "string" } },
233
+ "note": { "type": "string" }
234
+ }
235
+ },
236
+ "journalRound": {
237
+ "type": "object",
238
+ "required": ["entries"],
239
+ "additionalProperties": false,
240
+ "properties": {
241
+ "round": { "type": "integer", "minimum": 1, "description": "The dispatcher's round it was printed in." },
242
+ "state": { "type": "string", "description": "The state the plan was in when the round started." },
243
+ "entries": { "type": "array", "items": { "$ref": "#/$defs/journalEntry" } }
244
+ }
245
+ },
246
+ "journalEntry": {
247
+ "type": "object",
248
+ "required": ["kind", "text"],
249
+ "additionalProperties": false,
250
+ "properties": {
251
+ "kind": { "enum": ["todo", "done", "decision", "question", "override", "note"] },
252
+ "text": { "type": "string", "minLength": 1 },
253
+ "overrides": { "type": "integer", "minimum": 1, "description": "On an override: the number of the entry it overturns." },
254
+ "ref": { "type": "string", "pattern": "^B[0-9]+$", "description": "The blocked.md question this records, written by the harness." },
255
+ "answer": { "type": "string", "description": "A person's answer to that question, written by the harness." }
256
+ }
257
+ },
97
258
  "agent": { "type": "string", "pattern": "^[\\w-]+$" },
98
259
  "status": { "enum": ["Started", "Completed", "Almost completed", "Interrupted", "Blocked"] },
99
260
  "effort": { "enum": ["low", "medium", "high", "xhigh", "max", null] }
@@ -28,10 +28,10 @@ description: The openlanes software factory (`openlanes` CLI) that drives `.plan
28
28
 
29
29
  ## The model in four facts
30
30
 
31
- - **The folder name is the state.** `.plans/003.00-⭐️ → tax-rule-dsl` is plan `003.00`, state Planned, slug `tax-rule-dsl`. There is no database. Folder names hold emoji and spaces, so quote every path. Refer to a plan by its ordinal (`003`, `003.01`) wherever a command accepts one.
31
+ - **The folder name is the state.** `.plans/003.00━━⭐━━❯tax-rule-dsl` is plan `003.00`, state Planned, slug `tax-rule-dsl`. There is no database. Folder names hold emoji but no spaces; finished, deferred and discarded plans live in dated `.plans/archive/`, `deferred/` and `abandoned/` subfolders. Refer to a plan by its ordinal (`003`, `003.01`) wherever a command accepts one.
32
32
  - **Numbers are identity.** `NNN.00` is a new feature; `NNN.MM` with `MM > 0` documents work that already shipped (Retroactive). An ordinal is never renumbered.
33
33
  - **Lanes route a plan.** `full`: research, spec, plan, build, review. `plan`: the author's spec stands; plan, build, review. `quick`: one agent (`r2d2-mechanic`) does a short mechanical change, then review. The lane lives in `spec.md` frontmatter.
34
- - **Blocked means a human decides.** ⭕️ is a technical block (engineer), 🅱️ a product block (product). The questions sit in the plan's `blocked.md`; agents step around the plan until they are answered.
34
+ - **Blocked means a human decides.** ⭕ is a technical block (engineer), 🛑 a product block (product). The questions sit in the plan's `blocked.md`; agents step around the plan until they are answered.
35
35
 
36
36
  Live references, never copied here so they cannot go stale:
37
37
 
@@ -21,12 +21,12 @@ Ask the tool, never a copy of its table:
21
21
  | File | For | Written by |
22
22
  | --- | --- | --- |
23
23
  | `spec.md` | what and why; its frontmatter says how to route and judge the work | `openlanes create`, the human, `leah-researcher` (`## Research`), `yoda-writer` |
24
- | `plan.md` | units, order, the integration check; blank means "waiting for the planner" | `yoda-writer` creates it empty, `palpatine-planner` fills it |
25
- | `plan-backend.md`, `plan-frontend.md` | each builder's units; a front-end file with no unit heading starts nobody | `palpatine-planner` |
24
+ | `plan.md` | units under `## Backend` and `## Frontend`, order, the integration check; blank means "waiting for the planner"; a `## Frontend` with no unit heading starts nobody | `yoda-writer` creates it empty, `luthen-rael-planner` fills it |
25
+ | `contract.md`, `openapi.yaml` | the interfaces between the halves; the HTTP surface when there is one | `luke-backend` |
26
26
  | `blocked.md` | open questions a human must decide | any agent, or a human |
27
27
  | `pull-requests.md` | the pull request table the review states are judged by | `openlanes run` when it opens the PR, `openlanes create --prs`, `openlanes resync prs` |
28
28
  | `rewrite.md`, `delayed.md`, `discarded.md`, `deployed.md`, `rollback.md` | why the plan left the main path | the reviewer (`rewrite.md`), otherwise a human |
29
- | `state.json` | stages, signatures, mailbox | the harness only; read it with `openlanes state show --plan <NNN>` |
29
+ | `state.json` | stages, signatures, mailbox, read receipts, resume records (`last-known-state`) | the harness only; read it with `openlanes state show --plan <NNN>` |
30
30
 
31
31
  Agents also sign a ledger inside the document they own: a `> [!NOTE]` block of `[ agent: … status: Started, round 1 ]` lines. Leave those blocks as they are; the harness reads them to decide who finished.
32
32
 
@@ -36,7 +36,7 @@ Agents also sign a ledger inside the document they own: a `> [!NOTE]` block of `
36
36
 
37
37
  **blocked.md questions and answers.** A question is a line that starts with `B<n>`, after at most four `#` and an optional `**`: `## B1 — Which rate feed?`. Its answer is a heading `## A1` with the same number. Any other numbering (`1.`, `Q1`, a bullet `- B1`) is invisible: the folder never blocks, and `openlanes unblock` finds nothing to drain. Start no other line with `B` and a digit. An answer counts only when it names who decided (a role will do), carries a date, and picks one option.
38
38
 
39
- Which human a block waits on lives only in the emoji. `resync dirs` files a new `blocked.md` as ⭕️ (engineering); rename the folder's emoji to 🅱️ by hand when product owns the decision, and `resync` keeps it there.
39
+ Which human a block waits on lives only in the emoji. `resync dirs` files a new `blocked.md` as ⭕ (engineering); rename the folder's emoji to 🛑 by hand when product owns the decision, and `resync` keeps it there.
40
40
 
41
41
  **pull-requests.md.** openlanes reads the first markdown table whose header names a pull request (`Pull Request Number`, `PR`, `#`) and whose rows link to `/pull/<n>`. Columns are found by header words: number, name or title, status. Status words: `Open`, `Draft` or `WIP`, `Merged`, `Closed`. Escape `[`, `]` and `|` inside a title. Prose below the table is ignored.
42
42
 
@@ -1,6 +1,6 @@
1
1
  # blocked.md template
2
2
 
3
- A plan with `blocked.md` naming at least one open question is blocked: ⭕️ Technical Block (an engineer or the CTO decides) or 🅱️ Product Block (a product manager decides). Agents step around it until a human answers and runs `openlanes unblock <NNN>`, which hands the file to `lando-broker`. The broker folds each qualifying answer into `spec.md` (what and why) or `plan.md` (how and in what order), deletes the question with its answer, and deletes the file when nothing is left.
3
+ A plan with `blocked.md` naming at least one open question is blocked: ⭕ Technical Block (an engineer or the CTO decides) or 🛑 Product Block (a product manager decides). Agents step around it until a human answers and runs `openlanes unblock <NNN>`, which hands the file to `lando-broker`. The broker folds each qualifying answer into `spec.md` (what and why) or `plan.md` (how and in what order), deletes the question with its answer, and deletes the file when nothing is left.
4
4
 
5
5
  Two heading kinds, and only these: `## B<n>` for a question, `## A<n>` for its answer. openlanes counts the `B` lines; a question numbered any other way blocks nothing.
6
6
 
@@ -45,5 +45,5 @@ Notes:
45
45
 
46
46
  - An answer counts only when it names who decided (a role is enough), carries a date, and picks one option. "Leaning towards B" stays, and so does its question.
47
47
  - Answer in pieces if that is how answers arrive; whatever is still open keeps the plan blocked.
48
- - A new `blocked.md` lands at ⭕️ on the next `openlanes resync dirs`. Rename the folder's emoji to 🅱️ when product owns the decision; nothing else records which human it waits on.
48
+ - A new `blocked.md` lands at ⭕ on the next `openlanes resync dirs`. Rename the folder's emoji to 🛑 when product owns the decision; nothing else records which human it waits on.
49
49
  - `openlanes unblock <NNN> -n` lists the questions openlanes can see. Run it after writing the file.
@@ -14,7 +14,7 @@ Dropped for good; terminal. Say why, so nobody re-proposes it blind.
14
14
  **What would change our minds:** a regulator requiring live rates.
15
15
  ```
16
16
 
17
- ## delayed.md — ☢️ Deferred
17
+ ## delayed.md — 💤 Deferred
18
18
 
19
19
  The work could proceed and we chose not to yet. openlanes refuses the state unless the file names a **trigger** (it looks for the words trigger, revisit, when, until or once).
20
20
 
@@ -1,8 +1,8 @@
1
1
  # plan.md template
2
2
 
3
- `palpatine-planner` writes `plan.md` from a finished `spec.md`, plus `plan-backend.md` and `plan-frontend.md` holding each builder's units. Write it by hand only when you are taking the planner's place (lane `plan` skips research, not planning).
3
+ `luthen-rael-planner` writes `plan.md` from a finished `spec.md`, with each builder's units under `## Backend` and `## Frontend`. Write it by hand only when you are taking the planner's place (lane `plan` skips research, not planning).
4
4
 
5
- `plan.md` stays **blank** until it is a plan: any `#`, `##` or `###` heading outside a ledger block tells openlanes the plan is written, and moves the folder from 📋 Ready for Planning to ⭐️ Planned. The whole plan lands as one pull request; more than eight units means the spec is several plans.
5
+ `plan.md` stays **blank** until it is a plan: any `#`, `##` or `###` heading outside a ledger block tells openlanes the plan is written, and moves the folder from 📋 Ready for Planning to ⭐ Planned. The whole plan lands as one pull request; more than eight units means the spec is several plans.
6
6
 
7
7
  ```markdown
8
8
  # Rate Feed Sync — Plan
@@ -10,7 +10,7 @@
10
10
  **Spec:** `spec.md` in this folder
11
11
  **Depends on:** plans that must land first, by number, or nothing
12
12
 
13
- ## Units
13
+ ## Backend
14
14
 
15
15
  ### U1 — Rates table and model
16
16
 
@@ -35,6 +35,14 @@
35
35
  - **Depends on:** U2
36
36
  - **Done when:** a real sync, nothing stubbed but the vendor's HTTP, stores rows the model reads back
37
37
 
38
+ ## Frontend
39
+
40
+ No front-end units: the sync has no interface.
41
+
42
+ ## HTTP surface
43
+
44
+ None, so no `openapi.yaml`.
45
+
38
46
  ## Order
39
47
 
40
48
  U1 → U2 → U3. Units that write the same file are sequential; say which can run in parallel and why.
@@ -56,6 +64,6 @@ U1 → U2 → U3. Units that write the same file are sequential; say which can r
56
64
 
57
65
  Notes:
58
66
 
59
- - Every unit appears in exactly one of `plan-backend.md` and `plan-frontend.md`, under its own `##` or `###` heading. `rey-frontend` starts only when `plan-frontend.md` has a unit heading, so a plan with no front end writes one plain line there and no heading.
67
+ - Every unit appears in exactly one of `## Backend` and `## Frontend`, under its own `###` heading. `rey-frontend` starts only when `## Frontend` has a unit heading, so a plan with no front end writes one plain line there and no heading. Folders planned before plan 003 keep `plan-backend.md` and `plan-frontend.md`, which are still read.
60
68
  - `## Task completed when` and `## How to verify` are appended by the harness when missing; a section you wrote is left alone.
61
69
  - A plan stays one level above the code: signatures, routes and response shapes, not method bodies.
@@ -36,7 +36,7 @@ If the pull requests merged after 018's folder was created and before 019's, the
36
36
 
37
37
  ## What happens next
38
38
 
39
- `create --prs` writes `pull-requests.md` (the folder is born 🕰️ Retroactive) and hands the folder to `yoda-writer`, which writes `spec.md` from the diff and an empty `plan.md`. `--no-spec` stops after recording the PRs. From there the plan moves like any other; `openlanes run -n --plan <NNN.MM>` shows who takes it next.
39
+ `create --prs` writes `pull-requests.md` (the folder is born ⏪ Retroactive) and hands the folder to `yoda-writer`, which writes `spec.md` from the diff and an empty `plan.md`. `--no-spec` stops after recording the PRs. From there the plan moves like any other; `openlanes run -n --plan <NNN.MM>` shows who takes it next.
40
40
 
41
41
  ## What does not deserve one
42
42
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: openlanes-run
3
- description: Start the openlanes factory with `openlanes start` (engine plus control room, up until Stop All Local or Ctrl-C, questions answered and plans created on the page), or run openlanes's specialist agents to a fixed point with `openlanes run --until-settled` and clear blocked plans with `openlanes unblock`. Use when the user wants to start, run, resume or finish the agents, build or ship a plan, or unblock a ⭕️ / 🅱️ plan.
3
+ description: Start the openlanes factory with `openlanes start` (engine plus control room, up until Stop All Local or Ctrl-C, questions answered and plans created on the page), or run openlanes's specialist agents to a fixed point with `openlanes run --until-settled` and clear blocked plans with `openlanes unblock`. Use when the user wants to start, run, resume or finish the agents, build or ship a plan, or unblock a ⭕ / 🛑 plan.
4
4
  ---
5
5
 
6
6
  # openlanes-run: drive the plans
@@ -9,7 +9,7 @@ Load the `openlanes` skill first if it is not in context.
9
9
 
10
10
  ## `openlanes start`: the factory with its control room
11
11
 
12
- `openlanes start` is the way to run the factory for a person. It is `openlanes run` with the control room on, and it does not stop at a fixed point: when a plan blocks, palpatine-planner's questions included, it idles, shows the questions in **Needs You** on the page, takes the answer from there, drains it, and hands the plan back to its agent. It picks up plans created on the page, by `openlanes create` in another terminal, or edited by hand, rescanning `.plans/` every 5 s (`--rescan`).
12
+ `openlanes start` is the way to run the factory for a person. It is `openlanes run` with the control room on, and it does not stop at a fixed point: when a plan blocks, luthen-rael-planner's questions included, it idles, shows the questions in **Needs You** on the page, takes the answer from there, drains it, and hands the plan back to its agent. It picks up plans created on the page, by `openlanes create` in another terminal, or edited by hand, rescanning `.plans/` every 5 s (`--rescan`).
13
13
 
14
14
  ```bash
15
15
  openlanes start # engine and control room on http://127.0.0.1:11000, browser opened
@@ -52,7 +52,7 @@ Keep the defaults (`--isolation worktree`, pushing on) whenever the user wants p
52
52
  openlanes run --until-settled --plan <NNN,...>
53
53
  ```
54
54
 
55
- A run takes minutes to hours: each agent may take up to 15 minutes per round, over several rounds. Start it in the background and wait for it to exit; it prints its progress log path at the start, which is what to read when the user asks how it is going. Avoid polling in a tight loop.
55
+ A run takes minutes to hours: each agent may take up to 15 minutes per round, over several rounds. Start it in the background and wait for it to exit; it prints its log path at the start (`log/development.log` under the project unless `--log` says otherwise), which is what to read when the user asks how it is going. Avoid polling in a tight loop.
56
56
 
57
57
  When Redis answers, the run also serves the control room (it still ends at the fixed point, because of `--until-settled`) on `http://127.0.0.1:11000` and opens it in the browser: the user watches the board there and can message, interrupt or stop an agent. Without Redis the run warns once and goes on headless; that is not a failure. To get the page, start `redis-server` (or pass `--redis-url`) before the run; `openlanes-setup` has the details.
58
58
 
@@ -63,7 +63,7 @@ A warning about `ANTHROPIC_API_KEY` (or a similar variable) at the start means e
63
63
  When it exits, `openlanes status` shows where each plan stands. Sort them into three groups:
64
64
 
65
65
  - **Advanced to a human gate**: at ✅ Approved & Merged the reviewer has approved, and merging and deploying are the human's. Report the PR links (`gh pr list`).
66
- - **Blocked** ⭕️ / 🅱️: go to step 4.
66
+ - **Blocked** ⭕ / 🛑: go to step 4.
67
67
  - **Unmoved without a block**: an agent failed or ran out of rounds. Read the plan's history with `openlanes state show --plan <NNN>` and say which agent stopped and why. Re-run with a fix (`--agent`, `--prompt`, `--timeout`) only after telling the user what you will change.
68
68
 
69
69
  ## 4. Unblock
@@ -71,7 +71,7 @@ When it exits, `openlanes status` shows where each plan stands. Sort them into t
71
71
  For each blocked plan:
72
72
 
73
73
  1. `openlanes unblock <NNN> -n` prints the open questions.
74
- 1. Put them to the human **one at a time**. ⭕️ questions are for an engineer, 🅱️ for product; say which.
74
+ 1. Put them to the human **one at a time**. ⭕ questions are for an engineer, 🛑 for product; say which.
75
75
  1. Write each answer into the plan's `blocked.md` under `## A<n>`, matching its `## B<n>` (the format is in `openlanes-documents`). An answer counts only when it names who decided (a role is enough), carries today's date, and settles the question. "Leaning towards B" does not count, and the broker will leave it in place.
76
76
 
77
77
  ```markdown